From f157b3fb50a356a79f9fa52728d3568fa76b5d33 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Fri, 1 Oct 2021 11:45:00 -0700 Subject: [PATCH] Reinstate the getDefaultConsistency() method in the Configuration. Closes #1243. --- .../AbstractCouchbaseConfiguration.java | 12 +- .../couchbase/core/CouchbaseOperations.java | 6 + .../couchbase/core/CouchbaseTemplate.java | 18 ++- .../core/ReactiveCouchbaseOperations.java | 8 +- .../core/ReactiveCouchbaseTemplate.java | 16 +++ .../ReactiveFindByQueryOperationSupport.java | 3 +- ...ReactiveRemoveByQueryOperationSupport.java | 8 +- .../couchbase/domain/AirportRepository.java | 4 + ...chbaseRepositoryQueryIntegrationTests.java | 130 +++++++++++++++++- 9 files changed, 188 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 225a98d0d..45cdad9c4 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.Set; +import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; @@ -157,7 +158,8 @@ protected void configureEnvironment(final ClusterEnvironment.Builder builder) { @Bean(name = BeanNames.COUCHBASE_TEMPLATE) public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService); + return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, + getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, @@ -168,8 +170,8 @@ public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClien @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, - couchbaseTranslationService); + return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, + getDefaultConsistency()); } public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, @@ -379,4 +381,8 @@ private boolean nonShadowedJacksonPresent() { } } + public QueryScanConsistency getDefaultConsistency() { + return null; + } + } 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 3385278bd..33ce0791a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -19,6 +19,8 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}. */ @@ -44,4 +46,8 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { */ CouchbaseClientFactory getCouchbaseClientFactory(); + /** + * Returns the default consistency to use for queries + */ + QueryScanConsistency getConsistency(); } 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 2f1354dcc..67b261175 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -28,11 +28,11 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; import com.couchbase.client.java.Collection; +import com.couchbase.client.java.query.QueryScanConsistency; /** * Implements lower-level couchbase operations on top of the SDK with entity mapping capabilities. @@ -50,6 +50,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final MappingContext, CouchbasePersistentProperty> mappingContext; private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; + private QueryScanConsistency scanConsistency; public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { this(clientFactory, converter, new JacksonTranslationService()); @@ -57,11 +58,17 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final Couch public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, final TranslationService translationService) { + this(clientFactory, converter, translationService, null); + } + + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(converter, translationService); - this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService); - + this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService, + scanConsistency); + this.scanConsistency = scanConsistency; this.mappingContext = this.converter.getMappingContext(); if (mappingContext instanceof CouchbaseMappingContext) { CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext; @@ -136,6 +143,11 @@ public CouchbaseClientFactory getCouchbaseClientFactory() { return clientFactory; } + @Override + public QueryScanConsistency getConsistency() { + return scanConsistency; + } + /** * Provides access to a {@link Collection} on the configured {@link CouchbaseClientFactory}. * diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index 83e43bbe1..aee27b899 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -20,6 +20,8 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.support.PseudoArgs; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * Defines common operations on the Couchbase data source, most commonly implemented by * {@link ReactiveCouchbaseTemplate}. @@ -47,8 +49,12 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper CouchbaseClientFactory getCouchbaseClientFactory(); /** - * @@return the pseudoArgs from the ThreadLocal field of the CouchbaseOperations + * @return the pseudoArgs from the ThreadLocal field of the CouchbaseOperations */ PseudoArgs getPseudoArgs(); + /** + * @return the default consistency to use for queries + */ + QueryScanConsistency getConsistency(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 2aedc9dfc..3e27c1212 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -28,6 +28,7 @@ import org.springframework.data.couchbase.core.support.PseudoArgs; import com.couchbase.client.java.Collection; +import com.couchbase.client.java.query.QueryScanConsistency; /** * template class for Reactive Couchbase operations @@ -44,6 +45,7 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A private final PersistenceExceptionTranslator exceptionTranslator; private final ReactiveCouchbaseTemplateSupport templateSupport; private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); + private QueryScanConsistency scanConsistency; public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { this(clientFactory, converter, new JacksonTranslationService()); @@ -51,10 +53,16 @@ public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, fin public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, final TranslationService translationService) { + this(clientFactory, converter, translationService, null); + } + + public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); this.templateSupport = new ReactiveCouchbaseTemplateSupport(converter, translationService); + this.scanConsistency = scanConsistency; } @Override @@ -165,4 +173,12 @@ public PseudoArgs getPseudoArgs() { return threadLocalArgs == null ? null : threadLocalArgs.get(); } + /** + * {@inheritDoc} + */ + @Override + public QueryScanConsistency getConsistency() { + return scanConsistency; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 60a504384..0ff613dc8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -201,7 +201,8 @@ public Flux all() { @Override public QueryOptions buildOptions(QueryOptions options) { - QueryOptions opts = query.buildQueryOptions(options, scanConsistency); + QueryScanConsistency qsc = scanConsistency != null ? scanConsistency : template.getConsistency(); + QueryOptions opts = query.buildQueryOptions(options, qsc); return opts; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index a41bd7b91..a96bbed4b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,7 +15,6 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,6 +25,7 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -45,8 +45,7 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY,null, null, - null, null); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -94,7 +93,8 @@ public Flux all() { } private QueryOptions buildQueryOptions(QueryOptions options) { - return query.buildQueryOptions(options, scanConsistency); + QueryScanConsistency qsc = scanConsistency != null ? scanConsistency : template.getConsistency(); + return query.buildQueryOptions(options, qsc); } @Override diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index d849d32e3..0265fe7c8 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -43,6 +43,10 @@ @Repository public interface AirportRepository extends CouchbaseRepository { + // NOT_BOUNDED to test ScanConsistency + // @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED) + Airport iata(String iata); + @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAll(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index ac8d12d11..d0ccf83ff 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -16,13 +16,17 @@ package org.springframework.data.couchbase.repository; +import static com.couchbase.client.java.query.QueryScanConsistency.NOT_BOUNDED; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import java.lang.reflect.Method; import java.util.ArrayList; @@ -39,13 +43,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; @@ -194,14 +202,85 @@ void findBySimpleProperty() { } } + @Test + public void saveNotBounded() { + // save() followed by query with NOT_BOUNDED will result in not finding the document + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport airport2 = null; + for (int i = 1; i <= 100; i++) { + // set version == 0 so save() will be an upsert, not a replace + Airport saved = airportRepository.save(vie.clearVersion()); + try { + airport2 = airportRepository.iata(saved.getIata()); + if (airport2 == null) { + break; + } + } catch (DataRetrievalFailureException drfe) { + airport2 = null; // + } finally { + // airportRepository.delete(vie); + // instead of delete, use removeResult to test QueryOptions.consistentWith() + RemoveResult removeResult = couchbaseTemplate.removeById().one(vie.getId()); + assertEquals(vie.getId(), removeResult.getId()); + assertTrue(removeResult.getCas() != 0); + assertTrue(removeResult.getMutationToken().isPresent()); + Airport airport3 = airportRepository.iata(vie.getIata()); + assertNull(airport3, "should have been removed"); + } + } + assertNull(airport2, "airport2 should have likely been null at least once"); + Airport saved = airportRepository.save(vie.clearVersion()); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all(); + airport2 = airportRepository.iata(vie.getIata()); + RemoveResult removeResult = couchbaseTemplate.removeById().one(saved.getId()); + assertNotNull(airport2, "airport2 should have been found"); + } + + @Test + public void saveNotBoundedRequestPlus() { + ApplicationContext ac = new AnnotationConfigApplicationContext(ConfigRequestPlus.class); + // the Config class has been modified, these need to be loaded again + CouchbaseTemplate couchbaseTemplateRP = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + AirportRepository airportRepositoryRP = (AirportRepository) ac.getBean("airportRepository"); + + // save() followed by query with NOT_BOUNDED will result in not finding the document + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport airport2 = null; + for (int i = 1; i <= 100; i++) { + // set version == 0 so save() will be an upsert, not a replace + Airport saved = airportRepositoryRP.save(vie.clearVersion()); + try { + airport2 = airportRepositoryRP.iata(saved.getIata()); + if (airport2 == null) { + break; + } + } catch (DataRetrievalFailureException drfe) { + airport2 = null; // + } finally { + // airportRepository.delete(vie); + // instead of delete, use removeResult to test QueryOptions.consistentWith() + RemoveResult removeResult = couchbaseTemplateRP.removeById().one(vie.getId()); + assertEquals(vie.getId(), removeResult.getId()); + assertTrue(removeResult.getCas() != 0); + assertTrue(removeResult.getMutationToken().isPresent()); + Airport airport3 = airportRepositoryRP.iata(vie.getIata()); + assertNull(airport3, "should have been removed"); + } + } + assertNotNull(airport2, "airport2 should have never been null"); + Airport saved = airportRepositoryRP.save(vie.clearVersion()); + List airports = couchbaseTemplateRP.findByQuery(Airport.class).withConsistency(NOT_BOUNDED).all(); + RemoveResult removeResult = couchbaseTemplateRP.removeById().one(saved.getId()); + assertFalse(!airports.isEmpty(), "airports should have been empty"); + } + @Test void findByTypeAlias() { Airport vie = null; try { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); - List airports = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS) + List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) .matching(new Query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))).all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { @@ -252,7 +331,7 @@ public void testStreamQuery() { userRepository.save(user1); userRepository.save(user2); List users = userRepository.findByLastname("Wilson").collect(Collectors.toList()); - assertEquals(2,users.size()); + assertEquals(2, users.size()); assertTrue(users.contains(user1)); assertTrue(users.contains(user2)); userRepository.delete(user1); @@ -268,7 +347,7 @@ void count() { airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) .collect(Collectors.toSet())); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all(); Long count = airportRepository.countFancyExpression(asList("JFK"), asList("jfk"), false); assertEquals(1, count); @@ -427,7 +506,7 @@ void deleteAllById() { void couchbaseRepositoryQuery() throws Exception { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) + couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) .matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")).all(); String input = "findByFirstname"; Method method = UserRepository.class.getMethod(input, String.class); @@ -493,6 +572,47 @@ public NaiveAuditorAware testAuditorAware() { public DateTimeProvider testDateTimeProvider() { return new AuditingDateTimeProvider(); } + } + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + static class ConfigRequestPlus extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean(name = "auditorAwareRef") + public NaiveAuditorAware testAuditorAware() { + return new NaiveAuditorAware(); + } + + @Bean(name = "dateTimeProviderRef") + public DateTimeProvider testDateTimeProvider() { + return new AuditingDateTimeProvider(); + } + + @Override + public QueryScanConsistency getDefaultConsistency() { + return REQUEST_PLUS; + } + } }