diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index cba88abd..e42ede84 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -477,7 +477,7 @@ data, because the latest version hasn't been indexed yet. This gives the best pe Note that weaker consistencies can lead to data being returned that doesn't match the criteria of a derived query. One trickier case is when documents are deleted from Couchbase but views have not yet caught up to the deletion. With weak consistency this can mean that a view would return IDs that are not in the database anymore, leading to null entities. The `CouchbaseTemplate`s `findByView` and `findBySpatialView` methods will remove such stale deleted entities from their result in order to avoid having nulls in the returned collections. Similarly, `CouchbaseRepository`'s `deleteAll` method will ignore documents that the backing view provided but the SDK remove operation couldn't find. -If one wants to have stronger consistency, there are two possibilities described in the next sections. +If one wants to have stronger consistency, there are three possibilities described in the next sections. ==== Configure it on a global level A global consistency can be defined using the `Consistency` enumeration (eg. `Consistency.READ_YOUR_OWN_WRITE`): @@ -489,6 +489,35 @@ By default it is `Consistency.READ_YOUR_OWN_WRITES` (which means consistency is IMPORTANT: This is **only used in repositories**, either for index-backed methods automatically provided by the repository interface (`findAll()`, `findAll(keys)`, `count()`, `deleteAll()`...) or methods you define in your specific interface using query derivation. +==== Set consistency for N1QL queries +For N1QL queries, the `ScanConsistency` can be set using the `@WithConsistency` annotation. +This is independent of whether the query is derived from the method name or specified using `@Query`. +If the annotation is omitted, the global default consistency is used. +Consider the three queries in the following example: + +.Setting the scan consistency on repository methods +==== +[source,java] +---- +public interface UserRepository extends CrudRepository { + + List findByLastname(String name); <1> + + @WithConsistency(ScanConsistency.NOT_BOUNDED) + List findByFirstname(String name); <2> + + @WithConsistency(ScanConsistency.NOT_BOUNDED) + @Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}") + List findAllAdmins(); <3> +} +---- +==== +<1> Since neither `@Query` nor `@WithConsistency` is specified, a N1QL query is derived from the method name and executed with default consistency. +<2> The N1QL query is derived automatically but executed with the given consistency, i.e. `NOT_BOUNDED`. +<3> The N1QL query is specified using `@Query` and is being executed with the given consistency. + +IMPORTANT: The `@WithConsistency` annotation is currently only evaluated **for N1QL queries**. View queries use the global default consistency. + ==== Provide an implementation Provide the implementation and directly use `queryView` and `queryN1QL` methods on the template with a specific consistency (see <>). @@ -757,4 +786,4 @@ interface PersonRepository extends CrudRepository { } ---- -Projections declare a contract between the underlying type and the method signatures related to the exposed properties. Hence it is required to name getter methods according to the property name of the underlying type. If the underlying property is named `firstName`, then the getter method must be named `getFirstName` otherwise Spring Data is not able to look up the source property. \ No newline at end of file +Projections declare a contract between the underlying type and the method signatures related to the exposed properties. Hence it is required to name getter methods according to the property name of the underlying type. If the underlying property is named `firstName`, then the getter method must be named `getFirstName` otherwise Spring Data is not able to look up the source property. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java b/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java new file mode 100644 index 00000000..4e9ac15c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/WithConsistency.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2019 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 + * + * https://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.query; + +import com.couchbase.client.java.query.consistency.ScanConsistency; +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.config.AbstractCouchbaseConfiguration; + +/** + * Annotation to set the scan consistency of N1QL queries with Couchbase. + * This controls whether couchbase waits for all changes to be processed by an index + * or whether stale results are acceptable. + *

+ * If not set, the default consistency set in {@link AbstractCouchbaseConfiguration#getDefaultConsistency()} is used. + * + * @author Johannes Jasper. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@QueryAnnotation +public @interface WithConsistency { + + ScanConsistency value(); + +} 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 index 8d8ebd9a..9ae21216 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -16,11 +16,6 @@ package org.springframework.data.couchbase.repository.query; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.document.json.JsonValue; @@ -28,9 +23,11 @@ import com.couchbase.client.java.query.N1qlParams; import com.couchbase.client.java.query.N1qlQuery; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.consistency.ScanConsistency; +import java.util.Collection; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.domain.PageImpl; @@ -52,6 +49,7 @@ import org.springframework.util.Assert; * @author Simon Baslé * @author Subhashni Balakrishnan * @author Mark Paluch + * @author Johannes Jasper */ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { @@ -82,6 +80,15 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { protected abstract JsonValue getPlaceholderValues(ParameterAccessor accessor); + protected ScanConsistency getScanConsistency() { + + if (queryMethod.hasConsistencyAnnotation()) { + return queryMethod.getConsistencyAnnotation().value(); + } + + return getCouchbaseOperations().getDefaultConsistency().n1qlConsistency(); + } + @Override public Object execute(Object[] parameters) { ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); @@ -96,14 +103,12 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { JsonValue queryPlaceholderValues = getPlaceholderValues(accessor); //prepare the final query - N1qlQuery query = buildQuery(statement, queryPlaceholderValues, - getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); + N1qlQuery query = buildQuery(statement, queryPlaceholderValues, getScanConsistency()); //prepare a count query Statement countStatement = getCount(accessor, parameters); //the place holder values are the same for the count query as well - N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues, - getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); + N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues, getScanConsistency()); return processor.processResult(executeDependingOnType(query, countQuery, queryMethod, accessor.getPageable(), typeToRead)); } 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 3f7cb193..e425e86e 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 @@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProper import org.springframework.data.couchbase.core.query.Dimensional; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.View; +import org.springframework.data.couchbase.core.query.WithConsistency; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -144,6 +145,14 @@ public class CouchbaseQueryMethod extends QueryMethod { return getInlineN1qlQuery() != null; } + public boolean hasConsistencyAnnotation() { + return getConsistencyAnnotation() != null; + } + + public WithConsistency getConsistencyAnnotation() { + return method.getAnnotation(WithConsistency.class); + } + /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java index c376afdf..d728dfbd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQuery.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.repository.query; +import com.couchbase.client.java.query.consistency.ScanConsistency; import java.util.Map; import com.couchbase.client.java.document.json.JsonValue; import com.couchbase.client.java.query.N1qlQuery; @@ -31,6 +32,7 @@ import reactor.core.publisher.Flux; /** * @author Subhashni Balakrishnan * @author Mark Paluch + * @author Johannes Jasper * @since 3.0 */ public abstract class ReactiveAbstractN1qlBasedQuery implements RepositoryQuery { @@ -61,8 +63,7 @@ public abstract class ReactiveAbstractN1qlBasedQuery implements RepositoryQuery JsonValue queryPlaceholderValues = getPlaceholderValues(accessor); //prepare the final query - N1qlQuery query = N1qlUtils.buildQuery(statement, queryPlaceholderValues, - getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); + N1qlQuery query = N1qlUtils.buildQuery(statement, queryPlaceholderValues, getScanConsistency()); return ReactiveWrapperConverters.toWrapper( processor.processResult(executeDependingOnType(query, queryMethod, typeToRead)), Flux.class); } @@ -115,4 +116,13 @@ public abstract class ReactiveAbstractN1qlBasedQuery implements RepositoryQuery protected RxJavaCouchbaseOperations getCouchbaseOperations() { return this.couchbaseOperations; } + + protected ScanConsistency getScanConsistency() { + + if (queryMethod.hasConsistencyAnnotation()) { + return queryMethod.getConsistencyAnnotation().value(); + } + + return getCouchbaseOperations().getDefaultConsistency().n1qlConsistency(); + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/PartyRepository.java b/src/test/java/org/springframework/data/couchbase/repository/PartyRepository.java index 382c8907..d65d3686 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/PartyRepository.java +++ b/src/test/java/org/springframework/data/couchbase/repository/PartyRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.repository; +import com.couchbase.client.java.query.consistency.ScanConsistency; import java.util.Date; import java.util.List; @@ -24,6 +25,7 @@ import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.View; import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.couchbase.core.query.WithConsistency; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -49,6 +51,7 @@ public interface PartyRepository extends CouchbaseRepository { long countAllByDescriptionNotNull(); + @WithConsistency(ScanConsistency.NOT_BOUNDED) @Query("SELECT MAX(attendees) FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}") long findMaxAttendees(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveUserRepository.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveUserRepository.java index aae6c32b..0b5aec91 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveUserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveUserRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.repository; +import com.couchbase.client.java.query.consistency.ScanConsistency; import java.util.List; import com.couchbase.client.java.view.ViewQuery; @@ -24,6 +25,7 @@ import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.View; import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.couchbase.core.query.WithConsistency; import reactor.core.publisher.Flux; /** 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 index 35c4b2c1..b042b6df 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java @@ -27,9 +27,14 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.stream.Stream; +import com.couchbase.client.java.document.json.JsonValue; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.query.Consistency; +import org.springframework.data.couchbase.core.query.WithConsistency; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -38,6 +43,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import com.couchbase.client.java.document.json.JsonArray; @@ -48,6 +54,7 @@ import com.couchbase.client.java.query.ParameterizedN1qlQuery; import com.couchbase.client.java.query.SimpleN1qlQuery; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.consistency.ScanConsistency; +import org.springframework.data.repository.query.ReturnedType; /** * Unit tests for {@link AbstractN1qlBasedQuery}. @@ -266,6 +273,32 @@ public class AbstractN1qlBasedQueryTest { verify(mock).executeSingleProjection(any(N1qlQuery.class)); } + @Test // DATACOUCH-206 + public void shouldPickConsistencyFromAnnotation() throws NoSuchMethodException { + Class repositoryClass = SampleRepository.class; + + CouchbaseQueryMethod defaultQueryMethod = new CouchbaseQueryMethod(repositoryClass.getMethod("findAll"), + metadata, + projectionFactory, + context); + + + CouchbaseQueryMethod unboundedQueryMethod = new CouchbaseQueryMethod(repositoryClass.getMethod("streamAll"), + metadata, + projectionFactory, + context); + + CouchbaseTemplate template = mock(CouchbaseTemplate.class); + when(template.getDefaultConsistency()).thenReturn(Consistency.STRONGLY_CONSISTENT); + + ScanConsistency defaultConsistency = new SampleQuery(defaultQueryMethod, template).getScanConsistency(); + assertEquals(defaultConsistency, Consistency.STRONGLY_CONSISTENT.n1qlConsistency()); + + ScanConsistency unboundedConsistency = new SampleQuery(unboundedQueryMethod, template).getScanConsistency(); + assertEquals(unboundedConsistency, ScanConsistency.NOT_BOUNDED); + + } + static class Sample { Integer id; } @@ -276,6 +309,7 @@ public class AbstractN1qlBasedQueryTest { Sample findById(Integer id); + @WithConsistency(ScanConsistency.NOT_BOUNDED) Stream streamAll(); Page findAllPaged(Pageable pageable); @@ -288,4 +322,32 @@ public class AbstractN1qlBasedQueryTest { long longMethod(); } + + class SampleQuery extends AbstractN1qlBasedQuery { + + protected SampleQuery(CouchbaseQueryMethod queryMethod, + CouchbaseOperations couchbaseOperations) { + super(queryMethod, couchbaseOperations); + } + + @Override + protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) { + return null; + } + + @Override + protected boolean useGeneratedCountQuery() { + return false; + } + + @Override + protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) { + return null; + } + + @Override + protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { + return null; + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQueryTest.java new file mode 100644 index 00000000..45a82a82 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveAbstractN1qlBasedQueryTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2019 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 + * + * https://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 static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import com.couchbase.client.java.document.json.JsonValue; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.consistency.ScanConsistency; +import org.junit.*; +import org.springframework.data.couchbase.core.RxJavaCouchbaseOperations; +import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.query.Consistency; +import org.springframework.data.couchbase.core.query.WithConsistency; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ReturnedType; +import reactor.core.publisher.Flux; + +/** + * Unit tests for {@link ReactiveAbstractN1qlBasedQuery}. + * + * @author Johannes Jasper + */ +public class ReactiveAbstractN1qlBasedQueryTest { + + CouchbaseMappingContext context = new CouchbaseMappingContext(); + ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + RepositoryMetadata metadata = DefaultRepositoryMetadata.getMetadata(SampleRepository.class); + + + @Test // DATACOUCH-206 + public void shouldPickConsistencyFromAnnotation() throws NoSuchMethodException { + Class repositoryClass = SampleRepository.class; + + CouchbaseQueryMethod defaultQueryMethod = new CouchbaseQueryMethod(repositoryClass.getMethod("findAll"), + metadata, + projectionFactory, + context); + + + CouchbaseQueryMethod unboundedQueryMethod = new CouchbaseQueryMethod(repositoryClass.getMethod("findByName"), + metadata, + projectionFactory, + context); + + RxJavaCouchbaseTemplate template = mock(RxJavaCouchbaseTemplate.class); + when(template.getDefaultConsistency()).thenReturn(Consistency.STRONGLY_CONSISTENT); + + ScanConsistency defaultConsistency = new SampleQuery(defaultQueryMethod, template).getScanConsistency(); + assertEquals(defaultConsistency, Consistency.STRONGLY_CONSISTENT.n1qlConsistency()); + + ScanConsistency unboundedConsistency = new SampleQuery(unboundedQueryMethod, template).getScanConsistency(); + assertEquals(unboundedConsistency, ScanConsistency.NOT_BOUNDED); + + } + + static class Sample { + Integer name; + } + + interface SampleRepository extends ReactiveCouchbaseRepository { + + Flux findAll(); + + @WithConsistency(ScanConsistency.NOT_BOUNDED) + Flux findByName(); + } + + class SampleQuery extends ReactiveAbstractN1qlBasedQuery { + + protected SampleQuery(CouchbaseQueryMethod queryMethod, + RxJavaCouchbaseOperations couchbaseOperations) { + super(queryMethod, couchbaseOperations); + } + + @Override + protected Statement getStatement(ParameterAccessor accessor, + Object[] runtimeParameters, + ReturnedType returnedType) { + return null; + } + + @Override + protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { + return null; + } + } +}