From 6ec138182da008c7be465fb5e31f0e210e3b00ca Mon Sep 17 00:00:00 2001 From: yyfMichaelYan <54760415+yyfMichaelYan@users.noreply.github.com> Date: Mon, 8 Nov 2021 13:40:24 -0600 Subject: [PATCH 01/19] fix flaky test writesAndReadsCustomFieldsConvertedClass (#1264) From af66ada42cd19ce8d6ac989188686155d3a8181a Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 12 Nov 2021 10:49:16 +0100 Subject: [PATCH 02/19] Prepare 4.3 GA (2021.1.0). See #1257 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d3301d94..f579d911b 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-release + https://repo.spring.io/libs-release sonatype-snapshot From 00e05e4b41414afd815cf08127da13c2bc3481d1 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 12 Nov 2021 10:59:45 +0100 Subject: [PATCH 03/19] After release cleanups. See #1257 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f579d911b..0d3301d94 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-release - https://repo.spring.io/libs-release + spring-libs-snapshot + https://repo.spring.io/libs-snapshot sonatype-snapshot From ef22b3d7967f805d74ddb92e571899570cd80762 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Fri, 12 Nov 2021 07:15:45 -0800 Subject: [PATCH 04/19] Handle Collection<> parameters to repository query methods. (#1271) Closes #1270. Co-authored-by: mikereiche --- .../data/couchbase/core/query/QueryCriteria.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index 190105e09..4b89578a0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -25,6 +25,8 @@ import java.util.Locale; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseList; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -33,6 +35,7 @@ import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.json.JsonValue; +import org.springframework.util.CollectionUtils; /** * @author Michael Nitschinger From 5dbf182fd10c5405b7a9361f50188144b76239b8 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Thu, 6 Jan 2022 15:00:49 -0800 Subject: [PATCH 05/19] Incorporate changes from 5.0.x and bump Couchbase SDK. (#1287) Closes #1286. Co-authored-by: mikereiche --- .../core/CouchbaseTemplateSupport.java | 6 + .../couchbase/core/query/QueryCriteria.java | 3 - .../StringN1qlQueryCreatorMockedTests.java | 181 ++++++++++++++++++ 3 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index c6ec8a72d..44a81d15b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -150,6 +150,12 @@ public T decodeEntity(String id, String source, Long cas, Class entityCla N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection); return accessor.getBean(); } + CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { + if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { + return null; + } + return mappingContext.getPersistentEntity(entityClass); + } CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index 4b89578a0..190105e09 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -25,8 +25,6 @@ import java.util.Locale; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbaseList; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -35,7 +33,6 @@ import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.json.JsonValue; -import org.springframework.util.CollectionUtils; /** * @author Michael Nitschinger diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java new file mode 100644 index 000000000..66f124e3a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2017-2022 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; + +import java.lang.reflect.Method; +import java.util.Properties; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +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.query.Query; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ +class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests { + + MappingContext, CouchbasePersistentProperty> context; + CouchbaseConverter converter; + CouchbaseTemplate couchbaseTemplate; + static NamedQueries namedQueries = new PropertiesBasedNamedQueries(new Properties()); + + @BeforeEach + public void beforeEach() { + context = new CouchbaseMappingContext(); + converter = new MappingCouchbaseConverter(context); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + } + + @Test + void createsQueryCorrectly() throws Exception { + String input = "getByFirstnameAndLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + assertEquals( + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and firstname = $1 and lastname = $2", + query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); + } + + @Test + void createsQueryCorrectly2() throws Exception { + String input = "getByFirstnameOrLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + assertEquals( + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and (firstname = $first or lastname = $last)", + query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); + } + + @Test + void wrongNumberArgs() throws Exception { + String input = "getByFirstnameOrLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + try { + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + } catch (IllegalArgumentException e) { + return; + } + fail("should have failed with IllegalArgumentException: Invalid number of parameters given!"); + } + + @Test + void doesNotHaveAnnotation() throws Exception { + String input = "findByFirstname"; + Method method = UserRepository.class.getMethod(input, String.class); + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + try { + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + } catch (IllegalArgumentException e) { + return; + } + fail("should have failed with IllegalArgumentException: query has no inline Query or named Query not found"); + } + + private ParameterAccessor getAccessor(Parameters params, Object... values) { + return new ParametersParameterAccessor(params, values); + } + + private Parameters getParameters(Method method) { + return new DefaultParameters(method); + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + static class Config 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(); + } + + } +} From ecadff644c31a834d83c2061cb45edb43f2e8983 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 14 Jan 2022 10:57:51 +0100 Subject: [PATCH 06/19] Prepare 4.4 M1 (2021.2.0). See #1298 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d3301d94..61730fe6d 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-milestone + https://repo.spring.io/libs-milestone sonatype-snapshot From ebe33cf138970ad47575452bd8b1fc181964fba7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 14 Jan 2022 11:08:04 +0100 Subject: [PATCH 07/19] After release cleanups. See #1298 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 61730fe6d..0d3301d94 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-milestone - https://repo.spring.io/libs-milestone + spring-libs-snapshot + https://repo.spring.io/libs-snapshot sonatype-snapshot From da7d241e3927412b5ce32b9a1d017a2412c1891e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 18 Jan 2022 09:09:02 +0100 Subject: [PATCH 08/19] Prepare 4.4 M2 (2021.2.0). See #1301 --- pom.xml | 4 ++-- src/main/resources/notice.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0d3301d94..61730fe6d 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-milestone + https://repo.spring.io/libs-milestone sonatype-snapshot diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index dfb4fba45..6e09ad0ac 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -32,6 +32,5 @@ conditions of the subcomponent's license, as noted in the LICENSE file. - From 1fdbf6d5d091225d3297b3aab892013155000e06 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 18 Jan 2022 09:21:34 +0100 Subject: [PATCH 09/19] After release cleanups. See #1301 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 61730fe6d..0d3301d94 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-milestone - https://repo.spring.io/libs-milestone + spring-libs-snapshot + https://repo.spring.io/libs-snapshot sonatype-snapshot From 4a044197aa5d7c4bfc5d9b15f0dc7139bb4b4fc5 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Mon, 14 Feb 2022 09:10:04 -0800 Subject: [PATCH 10/19] Add mechanism for save to do one of insert, replace or upsert. (#1316) Closes #1277. --- ...aseRepositoryKeyValueIntegrationTests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index cd8d42589..bbeb31b49 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -87,6 +87,31 @@ void saveReplaceUpsertInsert() { airlineRepository.delete(airline).block(); } + @Autowired ReactiveAirlineRepository airlineRepository; + + @Test + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) + void saveReplaceUpsertInsert() { + // the User class has a version. + User user = new User(UUID.randomUUID().toString(), "f", "l"); + // save the document - we don't care how on this call + userRepository.save(user).block(); + // Now set the version to 0, it should attempt an insert and fail. + long saveVersion = user.getVersion(); + user.setVersion(0); + assertThrows(DuplicateKeyException.class, () -> userRepository.save(user).block()); + user.setVersion(saveVersion + 1); + assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user).block()); + userRepository.delete(user); + + // Airline does not have a version + Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline"); + // save the document - we don't care how on this call + airlineRepository.save(airline).block(); + airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. + airlineRepository.delete(airline).block(); + } + @Test void saveAndFindById() { User user = new User(UUID.randomUUID().toString(), "saveAndFindById_reactive", "l"); From 13d266e6f7ff06901c9359ef7aac5143858d22f1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Feb 2022 11:08:48 +0100 Subject: [PATCH 11/19] Prepare 4.4 M3 (2021.2.0). See #1307 --- pom.xml | 4 ++-- src/main/resources/notice.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d3301d94..61730fe6d 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-milestone + https://repo.spring.io/libs-milestone sonatype-snapshot diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 6e09ad0ac..dfb4fba45 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -32,5 +32,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file. + From e83bd41a86528ceb99d2291a23ac07fe6250b75f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Feb 2022 11:15:41 +0100 Subject: [PATCH 12/19] After release cleanups. See #1307 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 61730fe6d..0d3301d94 100644 --- a/pom.xml +++ b/pom.xml @@ -238,8 +238,8 @@ - spring-libs-milestone - https://repo.spring.io/libs-milestone + spring-libs-snapshot + https://repo.spring.io/libs-snapshot sonatype-snapshot From 9af8ece3708d805c03db8784bfd9cd3c77e76db9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Feb 2022 08:44:34 +0100 Subject: [PATCH 13/19] Use Java 17 to build snapshots for Artifactory. Closes #1352 --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3b6378fca..892e83be8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -67,7 +67,7 @@ pipeline { steps { script { docker.withRegistry(p['docker.registry'], p['docker.credentials']) { - docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { + docker.image(p['docker.java.lts.image']).inside(p['docker.java.inside.basic']) { sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase-non-root ' + '-Dartifactory.server=https://repo.spring.io ' + "-Dartifactory.username=${ARTIFACTORY_USR} " + From d5ca390d84a281b31854aa201cc53b4093af075d Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Tue, 22 Feb 2022 09:56:53 -0800 Subject: [PATCH 14/19] Reinstate CouchbaseCache documentation. (#1357) Closes #1356. --- .../java/org/springframework/data/couchbase/domain/Config.java | 2 ++ .../java/org/springframework/data/couchbase/domain/User.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 209b90bdc..27d50fc6f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -17,6 +17,8 @@ package org.springframework.data.couchbase.domain; import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index 5a841edb8..990a91a37 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -29,6 +29,8 @@ import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; +import java.io.Serializable; + /** * User entity for tests * From 5f03d1e0ce80e820ff8a3322b2ebfcf6f1bebb8f Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Tue, 22 Feb 2022 10:09:25 -0800 Subject: [PATCH 15/19] Fix update to cache documentation. (#1359) Closes #1358. --- src/main/asciidoc/caching.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/asciidoc/caching.adoc b/src/main/asciidoc/caching.adoc index 5aacff6a5..e0bfcfb37 100644 --- a/src/main/asciidoc/caching.adoc +++ b/src/main/asciidoc/caching.adoc @@ -22,13 +22,13 @@ To make it work, you need to add the `@EnableCaching` annotation and configure t public class Config extends AbstractCouchbaseConfiguration { // general methods - @Bean - public CouchbaseCacheManager cacheManager(CouchbaseTemplate couchbaseTemplate) throws Exception { - CouchbaseCacheManager.CouchbaseCacheManagerBuilder builder = CouchbaseCacheManager.CouchbaseCacheManagerBuilder - .fromConnectionFactory(couchbaseTemplate.getCouchbaseClientFactory()); - builder.withCacheConfiguration("mySpringCache", CouchbaseCacheConfiguration.defaultCacheConfig()); - return builder.build(); - } + @Bean + public CouchbaseCacheManager cacheManager(CouchbaseTemplate couchbaseTemplate) throws Exception { + CouchbaseCacheManager.CouchbaseCacheManagerBuilder builder = CouchbaseCacheManager.CouchbaseCacheManagerBuilder + .fromConnectionFactory(couchbaseTemplate.getCouchbaseClientFactory()); + return builder.build(); + } + ---- ==== From b02959dded4cf19b2f8b6f78b016b5bb6afb9ac8 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Mon, 14 Mar 2022 09:37:51 -0700 Subject: [PATCH 16/19] Support derived queries on repositories defined with an abstract entity class. (#1366) Motivation: Currently an abstract entity class specified in the repository definition can be used for the predicate typeKey = typeAlias(of abstract entity class) in queries. Since documents are stored with typeKey = typeAlias(of concrete class) those queries will never match any documents. To allow this to work, all of the abstract entity class an all concrete subclasses must use the same typeAlias. Once those documents are found, regardless of their concrete class, they will all have the same typeKey = typeAlias, instead of having the typeAlias specific to the concrete class. Additional information in the stored document is needed to identify the concrete class (subtype in the example test case), as well as a TypeMapper to interpret that information. Changes: This allows a common TypeAlias to be used for the purpose of the predicate typeKey = typeAlias, and the determination of the concrete type by implementing an AbstractingMappingCouchbaseConverter that inspects the 'subtype' property. Closes #1365. Co-authored-by: Michael Reiche --- .../springframework/data/couchbase/domain/User.java | 11 +++++++++++ .../query/StringN1qlQueryCreatorMockedTests.java | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index 990a91a37..a3cc29760 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -30,6 +30,17 @@ import org.springframework.data.couchbase.core.mapping.Document; import java.io.Serializable; +import java.util.Objects; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.Transient; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.core.mapping.Document; /** * User entity for tests diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java index 66f124e3a..516f4c9ca 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java @@ -85,7 +85,7 @@ void createsQueryCorrectly() throws Exception { Query query = creator.createQuery(); assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and firstname = $1 and lastname = $2", + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2", query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); } @@ -104,7 +104,7 @@ void createsQueryCorrectly2() throws Exception { Query query = creator.createQuery(); assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and (firstname = $first or lastname = $last)", + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)", query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); } From edadede7a01997a364c1f5169a36e8bb3e4eee11 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Fri, 24 Jun 2022 10:45:21 -0700 Subject: [PATCH 17/19] Add support for Couchbase Transactions. The fluent operations are common for options that are common to operations with and without transactions. Once there is a transaction(ctx), or an option specific to without-transactions (such as scanConsistency), the interfaces are bifurcated, so that an non-transaction option cannot be applied to a transaction operation, and a transaction(ctx) cannot be applied where a non-transaction option has already been applied. Closes #1145. Support for @Transactional for blocking and reactive Transactionmanager. Closes 1145. Transaction Support. Transaction Support. Porting to SDK-integrated version of transactions The transactions logic exists in the Java SDK as of 3.3.0, with a slightly different API. This is the first effort at the port, which literally just compiles. It will not run as crucial code has been commented and todo-ed. There is work remaining to figure out how to complete the port, as some crucial parts (such as ctx.commit() and ctx.rollback()) have been intentionally removed. Continuing work to get the ExtSDKIntegration port working Trying to transition to CallbackPreferring manager. Added CouchbaseSimpleCallbackTransactionManager, the simplest possible implementation of CallbackPreferringTransactionManager, combined with a simpler approach to ThreadLocal storage in ReactiveInsertByIdSupport. Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now passing. Adding WIP get-and-replace @Transactional support (Not yet working as CAS/version field in Person is not populated correctly.) Datacouch 1145 transaction support (#1423) * Porting to SDK-integrated version of transactions The transactions logic exists in the Java SDK as of 3.3.0, with a slightly different API. This is the first effort at the port, which literally just compiles. It will not run as crucial code has been commented and todo-ed. There is work remaining to figure out how to complete the port, as some crucial parts (such as ctx.commit() and ctx.rollback()) have been intentionally removed. * Continuing work to get the ExtSDKIntegration port working Trying to transition to CallbackPreferring manager. * Added CouchbaseSimpleCallbackTransactionManager, the simplest possible implementation of CallbackPreferringTransactionManager, combined with a simpler approach to ThreadLocal storage in ReactiveInsertByIdSupport. Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now passing. * Adding WIP get-and-replace @Transactional support (Not yet working as CAS/version field in Person is not populated correctly.) Commit before pulling Graham's changes. Merge branch 'datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into datacouch_1145_transaction_support Transitioning to use CoreTransactionAttemptContext. Tests may fail. Removing AttemptContextReactiveAccessor Don't think we need this, as we can pass around CoreTransactionAttemptContext instead, which gives access to a lot of internals. Removing TransactionsReactive Would prefer not to C&P a huge class out of the transaction internals, and don't think we need it. Removing some files not currently used To reduce & simplify the amount of code to look at. Some don't seem to be used in any branch, some just aren't used in this branch. Removing CouchbaseTransactionInterceptor As per offline discussion, CallbackPreferringPlatformTransactionManager is perhaps the optimal solution. Copying @Transactional tests out into separate class Tidyup Tidyup test names Verify GenericSupport is on same thread before and after transactional operation Refactoring CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage management Using latest java-client ReactiveReplaceByIdSupport - Fixing use of CAS now have CoreTransactionAttemptContext. Removing unused code. ReactiveInsertByIdSupport - fixing use of reactive vs non-reactive, and CAS Merging upstream Remove incorrect thread check (.doOnNext could execute on a different thread) Get scope and collection from pseudoArgs and some cleanup. Completing merge from upstream Removing unused classes Give GenericSupport a better name Reject at runtime options that aren't supported in a transaction Fixing some small todos, partly by removing unused coe Fix runtime option checks Simplifying CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage Standardising on ReactiveCouchbaseResourceHolder rather than holding CoreTransactionAttemptContext too Removing version from CouchbaseDocument Can't recall why I added this, and tests pass without it Improving CouchbaseTransactionalIntegrationTests and adding more tests Reject operations that aren't allowed in a transaction (upsertById etc.) Improve handling of CAS mismatch By calling CoreTransactionAttemptContext.operationFailed, it ensures that internal state is set. So even if the user catches the exception, the transaction still behaves as it should. Removing a now-redundant non-transactional check on upsertById I missed this when adding TransactionalSupport.verifyNotInTransaction here. Support @Transactional options timeout and isolation level Add ReactiveTransactionWrapper/TransactionWrapper and a bunch of cleanup. Merge branch 'datacouch_1145_transaction_support' of https://github.com/programmatix/spring-data-couchbase into programmatix-datacouch_1145_transaction_support Fixed up merge issues. Datacouch 1145 transaction support (#1447) * Move CouchbaseTransactionalOperator to use SLF4J, same as rest of the code. * Handle all propagation levels * Adding new tests for repository calls inside @Transactional One test is failure due to what looks like a bug elsewhere. * Rename CouchbaseTransactionalIntegrationTests, and check after each test that we're not in a transaction. Remove unnecessary methods From ReactiveCouchbaseClientFactory. Also rationalized naming of methods and other changes. Cleanup of test classes. Datacouch 1145 transaction support (#1448) * Move CouchbaseTransactionalOperator to use SLF4J, same as rest of the code. * Handle all propagation levels * Adding new tests for repository calls inside @Transactional One test is failure due to what looks like a bug elsewhere. * Rename CouchbaseTransactionalIntegrationTests, and check after each test that we're not in a transaction. * Remove unnecessary methods From ReactiveCouchbaseClientFactory. Also rationalized naming of methods and other changes. * Cleanup of test classes. * Removing unused classes (Reducing the cognitive burden) * Removing version from CouchbaseDocument This change was done previously - it must have slipped back in a merge. * Adding and removing TODOs * Adding and removing TODOs * DRYing CouchbaseTransactionalPropagationIntegrationTests * Check propagation tests retry as expected * Tidy up PersonWithoutVersion to the minimum required * Removing unused code This should all be non-destructive. Just removing code IntelliJ declares unused. Intent is to make it easier to figure out how the CoreTransactionAttemptContext TLS is working. * Adding tests for @Transactional removeByQuery and findByQuery Failing as they aren't being executed transactionally - investigating why. Co-authored-by: Michael Reiche <48999328+mikereiche@users.noreply.github.com> change references to resource holder change refs to resource holder Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support Removing core transaction attempt context bound couchbase client factory rebased (#1449) * Move ReactiveTransactionsWrapper tests into a new file * (Temporarily?) disabling tests using CouchbaseTransactionOperation or TransactionalOperator As I feel we should be removing/not-supporting these, and on this branch I've broken them. * Make all transaction tests call assertNotInTransaction * Removing unused code * Instead of binding the transaction AttemptContext to a CouchbaseClientFactory, fetch it from ThreadLocalStorage (or the reactive context) instead. This allows a lot of simplifying: * The non-trivial ReactiveCouchbaseClientUtils can be removed * As can CoreTransactionAttemptContextBoundCouchbaseClientFactory Also removing TransactionalSupport.one as it wasn't providing as much DRY utility as I thought it would - only used in two places. This change won't compile on its own. To reduce the complexity of this patchset, the Reactive*OperationSupport changes will go into a separate commit. * Reactive*OperationSupport changes to support the previous commit. * Fixing ReactiveRemoveByQuerySupport. Both to support the changes to TransactionalSupport. And to fix the TODO where the query resuls were not being handled. * Disabling a test * Adding CouchbaseTransactionsWrapperTemplateIntegrationTests * Another advantage of removing CoreTransactionAttemptContextBoundCouchbaseClientFactory is we can remove Cluster and ClusterInterface. * Adding CouchbaseReactiveTransactionsWrapperTemplateIntegrationTests Some of these tests are currently failing - tracking down where the issue is. Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support manual merges for PR manual merges for PR Fix a bunch of test cases and remove unused bits. Removing CouchbaseTransactionInterceptor As discussed on Slack. Reenabling some tests that are passing (Unclear why these were disabled?) Verified that CallbackPreferringPlatformTransactionManager getTransaction/commit/rollback are never called Adding tests for TransactionTemplate, which works fine with CouchbaseSimpleCallbackTransactionManager Whether we actually document this support is another matter - it's yet another way of doing transactions. Small fixes to support TransactionTemplate * Handle if the user has set isRollbackOnly on the TransactionStatus (which is only available - I think - when using TransactionTemplate) * Supply a `transaction` object to CouchbaseTransactionStatus so that status.isNewTransaction() correctly returns true. (This method requires that transaction to be set.) Clarifying that direct use of PlatformTransactionManager is not supported Adding further TransactionTemplateIntegrationTests tests Fixing removeByQuery queryOptions creation Removing now-fixed TODO (no longer key off Cluster) Adding overload to CouchbaseSimpleCallbackTransactionManager to allow it to be constructed without a TransactionOptions. Adding CouchbaseSimpleTransactionalOperator, the simplest possible implementation of TransactionalOperator. Adding retry tests Just making sure that error handling and retries are done correctly throughout. Updating and adding some TODOs Have CouchbaseTransactionManager support CouchbaseResourceHolder.class binding Adding more tests for CouchbaseTransactionManager. These tests fail, and are known to fail. I'm adding them as a solid demonstration of why I don't feel we can have this CouchbaseTransactionManager: it doesn't provide the crucial 'core loop' functionality, including error handling and retries. We should standardise on CouchbaseSimpleCallbackTransactionManager instead. CouchbaseSimpleCallbackTransactionManager.executeNewReactiveTransaction now buffers results rather than trying to stream a Flux from out of a completed lambda (which I doubt is even possible.) Removing comment that has been resolved. (As per Slack, we will live with this limitation.) Adding CouchbaseSimpleTransactionInterceptor, a very simple TransactionInterceptor implemention that defers to CouchbaseSimpleCallbackTransactionManager if that is the provided TransactionManager, and otherwise just calls super. This allows reactive @Transactional - though all @Transactional methods including blocking will now flow through it. There are two rather divergent approaches in the code currently: 1. CouchbaseTransactionManager, ReactiveTransactionManager, CouchbaseTransactionalOperator, CouchbaseTransactionInterceptor 2. CouchbaseSimpleCallbackTransactionManager, CouchbaseSimpleTransactionalOperator, CouchbaseSimpleTransactionInterceptor I know the intent is to remove some aspects of (1), but until that's done it's proving tricky to have tests for both concurrently - I've hit several issues on adding CouchbaseSimpleTransactionInterceptor, with 'multiple transaction manager beans in config' being common. So, temporarily moving some beans from AbstractCouchbaseConfiguration into the test Config class, renaming it, and having two separately TransactionsConfig classes for the two approaches. Once we've aligned the approaches more, can move what beans survive back into AbstractCouchbaseConfiguration. Safety check in CouchbaseSimpleCallbackTransactionManager that the blocking run is not accidentally running a reactive @Transactional somehow. Adding tests for reactive @Transactional, which now works (including error handling and retries) as of the CouchbaseSimpleTransactionInterceptor. With TransactionsConfigCouchbaseSimpleTransactionManager change, can now simplify @Transactional(transactionManager = ...) to just @Transactional. Tidying TODOs I saw in a PR comment that getResources no longer uses TransactionOptions Removing configureTransactions from config As per PR discussion Removing some code that has now been refactored into 3.3.1 SDK Removing TODOs that are TODONE already Removing transactionsOptions() bean from AbstractCouchbaseConfiguration. As per comment, this feels unnecessary: any options you'd configure at this config level, you'd surely provide at the global (Cluster) level instead? Reinstating some commented-out code This looks pretty crucial - can't recall why I commented it in first place Removing TransactionResult This was from a now-abandoned idea of storing transactional metadata in the entity class. Fix recent removal of TransactionOptions bean Switch some IllegalStateException for more accurate UnsupportedOperationException Tidying tests to remove old GenericApplicationContext method Tidying some TODOs (todo gp == in code that I think we should remove) (todo gpx == needs looking at) Tidying TransactionsConfigCouchbaseSimpleTransactionManager Provide SpringTransactionAttemptContext and ReactiveSpringTransactionAttemptContext wrappers For use by TransactionsWrapper and reactive equivalent. Pro: it's an abstraction layer. It lets us add Spring-specific functionality, or hide functionality that for whatever reason doesn't work with Spring (which is why it's composition rather than inheritance, beyond that being a best practice anyway). Con: any new API added will also have to be added to these wrappers. But that's a small amount of work and API is added very infrequently. Cleanup tests mostly. Temporary fix for CouchbaseSimpleCallbackTransactionManager. Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support Tidying up test cases. Fixes to over-zealous manual merging. - keep CouchbaseTransactionManager as Graham is still using it. - fix tests that were expecting SimulateFailure to be nested. Add ReactiveTransactionWrapper and enable tests that use it. More tidying. Removed CouchbaseTransactionManager. Only wrap exceptions in CouchbaseSimpleTransactionInterceptor if they are not RuntimeExceptions. Removed CouchbaseTransactionalOperator and ReactiveCouchbaseTransactionalOperator. Fixed non-transactions regression introduced in previous commit. Removing the two transactions config classes Now we've landed on a single agreed approach there's no need for this separation any more, and any beans can be moved back into AbstractCouchbaseConfiguration. Rename CouchbaseSimpleTransactionInterceptor The "Simple" moniker is no longer useful since we now only have one of these. Rename CouchbaseSimpleCallbackTransactionManager The "Simple" moniker is no longer useful since we now only have one of these. Rename CouchbaseSimpleTransactionalOperator The "Simple" moniker is no longer useful since we now only have one of these. Change CouchbaseTransactionalOperator construction to static As per Slack discussion. Remove comments related to another database Removing a test comment that doesn't seem to apply anymore Test passes for me at least Simplify TransactionsWrapper and AttemptContextReactiveAccessor newCoreTranactionAttemptContext is reimplementing some code from core plus has some config bugs to resolve. I think it's simpler to remove it and replace TransactionsWrapper (the only code still using this method) with the simple code seen now. (Note this is similar to how I had it before https://github.com/spring-projects/spring-data-couchbase/commit/1701183b63876c01cd4b012cbce7c783fc374e6b - not sure if that commit intentionally reverted things?) This change also petmits a lot of tidyup & simplification throughout the codebase. We can just create CouchbaseResourceHolders directly now. Tidying a test No longer needs retryWhen now using the new approaches Remove ReactiveCouchbaseClientFactory. It was added to support getting the transaction from the TransactionSynchronizationManager.forCurrent() and providing a template with a session containing the transaction. Fixing some code warnings Mostly removing unused code Mark internal classes @Stability.Internal Removing some tests that have already been previously moved into another file. Removing the transaction wrappers Requires JVMCBC-1105 and 3.3.2 Add more tests for native SDK transactions Move all tests related to native SDK transactions into their own package It makes it easier to test just that functionality while iterating. Removing CouchbaseTransactionManagerTransactionalTemplateIntegrationTests This test is now redundant. It was created to show why the original CouchbaseTransactionManager couldn't work (no retries). Now that has been replaced, this test is just duplicating others. Adding some minimal JavaDocs and comments. Tidying up after moving ThreadLocalStorage into SDK Deleting CouchbaseTemplateTransactionIntegrationTests As this relies on Spring test @Transactional, which we do not support as that Spring logic is not aware of CallbackPreferringTransactionManager. Removed some redundant bean names Tidying up tests and comments Removing some now-unused reflection code Starting with mapping TransactionFailedException and TransactionCommitAmbiguousException, into new errors TransactionSystemUnambiguousException and TransactionSystemAmbiguousException. These will be raised from an @Transactional transaction. E.g. to do error handling the user would do: ``` try { service.transactionalMethod(); } catch (TransactionSystemAmbiguousException ex) { // app-specific handling } catch (TransactionSystemUnambiguousException ex) { // app-specific handling } class Service { @Transactional void transactionalMethod() { // ... } } ``` Mapping TransactionOperationFailedException, which is an opaque signal raised from transaction operations, to new exception UncategorizedTransactionDataAccessException. This depends on some new functionality added into Java SDK 3.3.2, WrappedTransactionOperationFailedException. Minor tidyuo Improving tests for correct operation-level errors --- .mvn/wrapper/maven-wrapper.jar | Bin 48337 -> 0 bytes pom.xml | 21 +- .../AttemptContextReactiveAccessor.java | 36 ++ .../couchbase/CouchbaseClientFactory.java | 4 +- .../SimpleCouchbaseClientFactory.java | 24 +- .../AbstractCouchbaseConfiguration.java | 87 ++- .../data/couchbase/config/BeanNames.java | 6 + .../core/AbstractTemplateSupport.java | 208 ++++++++ .../core/CouchbaseExceptionTranslator.java | 9 + .../couchbase/core/CouchbaseOperations.java | 7 + .../couchbase/core/CouchbaseTemplate.java | 44 +- .../core/CouchbaseTemplateSupport.java | 101 +--- .../ExecutableFindByAnalyticsOperation.java | 101 ++-- ...utableFindByAnalyticsOperationSupport.java | 6 +- .../core/ExecutableFindByIdOperation.java | 71 ++- .../ExecutableFindByIdOperationSupport.java | 18 +- .../core/ExecutableFindByQueryOperation.java | 138 +++-- ...ExecutableFindByQueryOperationSupport.java | 18 +- .../core/ExecutableInsertByIdOperation.java | 49 +- .../ExecutableInsertByIdOperationSupport.java | 15 +- .../core/ExecutableRemoveByIdOperation.java | 61 ++- .../ExecutableRemoveByIdOperationSupport.java | 26 +- .../ExecutableRemoveByQueryOperation.java | 78 +-- ...ecutableRemoveByQueryOperationSupport.java | 11 +- .../core/ExecutableReplaceByIdOperation.java | 42 +- ...ExecutableReplaceByIdOperationSupport.java | 15 +- .../core/ExecutableUpsertByIdOperation.java | 33 +- .../core/NonReactiveSupportWrapper.java | 36 +- .../core/ReactiveCouchbaseOperations.java | 14 +- .../core/ReactiveCouchbaseTemplate.java | 112 +++- .../ReactiveCouchbaseTemplateSupport.java | 189 +------ .../ReactiveExistsByIdOperationSupport.java | 9 +- .../ReactiveFindByAnalyticsOperation.java | 98 ++-- ...activeFindByAnalyticsOperationSupport.java | 13 +- .../core/ReactiveFindByIdOperation.java | 71 ++- .../ReactiveFindByIdOperationSupport.java | 84 ++- .../core/ReactiveFindByQueryOperation.java | 127 +++-- .../ReactiveFindByQueryOperationSupport.java | 140 +++-- ...eFindFromReplicasByIdOperationSupport.java | 9 +- .../core/ReactiveInsertByIdOperation.java | 49 +- .../ReactiveInsertByIdOperationSupport.java | 86 ++- .../core/ReactiveRemoveByIdOperation.java | 57 +- .../ReactiveRemoveByIdOperationSupport.java | 81 ++- .../core/ReactiveRemoveByQueryOperation.java | 75 ++- ...ReactiveRemoveByQueryOperationSupport.java | 57 +- .../core/ReactiveReplaceByIdOperation.java | 40 +- .../ReactiveReplaceByIdOperationSupport.java | 97 +++- .../core/ReactiveTemplateSupport.java | 20 +- .../core/ReactiveUpsertByIdOperation.java | 33 +- .../ReactiveUpsertByIdOperationSupport.java | 34 +- .../data/couchbase/core/TemplateSupport.java | 19 +- .../couchbase/core/TransactionalSupport.java | 57 ++ .../convert/MappingCouchbaseConverter.java | 1 - .../BasicCouchbasePersistentEntity.java | 12 +- .../mapping/CouchbasePersistentEntity.java | 4 - .../event/ReactiveAuditingEntityCallback.java | 1 - .../couchbase/core/query/OptionsBuilder.java | 25 +- .../couchbase/core/support/PseudoArgs.java | 12 +- .../data/couchbase/core/support/WithCas.java | 33 ++ .../core/support/WithTransaction.java | 30 ++ .../repository/DynamicProxyable.java | 26 +- .../ReactiveCouchbaseRepository.java | 1 + .../support/CouchbaseRepositoryBase.java | 13 +- .../support/DynamicInvocationHandler.java | 18 +- .../SimpleReactiveCouchbaseRepository.java | 4 +- .../support/TransactionResultHolder.java | 56 ++ .../CouchbaseCallbackTransactionManager.java | 285 ++++++++++ .../transaction/CouchbaseResourceHolder.java | 58 ++ .../CouchbaseTransactionDefinition.java | 12 + .../CouchbaseTransactionInterceptor.java | 92 ++++ .../CouchbaseTransactionStatus.java | 30 ++ .../CouchbaseTransactionalOperator.java | 54 ++ ...TransactionRollbackRequestedException.java | 27 + .../TransactionSystemAmbiguousException.java | 40 ++ .../TransactionSystemCouchbaseException.java | 43 ++ ...TransactionSystemUnambiguousException.java | 30 ++ ...gorizedTransactionDataAccessException.java | 43 ++ ...chbaseCacheCollectionIntegrationTests.java | 3 + .../cache/CouchbaseCacheIntegrationTests.java | 4 +- ...hbaseTemplateKeyValueIntegrationTests.java | 10 +- ...mplateQueryCollectionIntegrationTests.java | 155 +++--- ...ouchbaseTemplateQueryIntegrationTests.java | 38 +- ...hbaseTemplateKeyValueIntegrationTests.java | 8 + ...mplateQueryCollectionIntegrationTests.java | 143 ++--- .../data/couchbase/domain/AbstractEntity.java | 4 + .../data/couchbase/domain/Airport.java | 1 + .../couchbase/domain/AirportRepository.java | 7 + .../couchbase/domain/CollectionsConfig.java | 8 + .../data/couchbase/domain/Config.java | 9 +- .../domain/FluxIntegrationTests.java | 44 +- .../data/couchbase/domain/Person.java | 74 ++- .../couchbase/domain/PersonRepository.java | 7 +- .../domain/PersonWithoutVersion.java | 46 ++ .../domain/ReactiveAirportRepository.java | 7 + .../domain/ReactivePersonRepository.java | 27 + .../data/couchbase/domain/UserRepository.java | 3 + ...aseRepositoryKeyValueIntegrationTests.java | 3 + ...chbaseRepositoryQueryIntegrationTests.java | 63 ++- ...chbaseRepositoryQueryIntegrationTests.java | 26 +- ...sitoryQueryCollectionIntegrationTests.java | 33 +- ...sitoryQueryCollectionIntegrationTests.java | 7 + .../query/StringN1qlQueryCreatorTests.java | 32 +- .../AfterTransactionAssertion.java | 33 ++ ...basePersonTransactionIntegrationTests.java | 350 ++++++++++++ ...onTransactionReactiveIntegrationTests.java | 242 +++++++++ ...uchbaseReactiveTransactionNativeTests.java | 228 ++++++++ .../CouchbaseTransactionNativeTests.java | 191 +++++++ ...onAllowableOperationsIntegrationTests.java | 137 +++++ ...ionalOperatorTemplateIntegrationTests.java | 333 ++++++++++++ ...eTransactionalOptionsIntegrationTests.java | 137 +++++ ...nsactionalPropagationIntegrationTests.java | 358 +++++++++++++ ...ansactionalRepositoryIntegrationTests.java | 147 +++++ ...TransactionalTemplateIntegrationTests.java | 503 ++++++++++++++++++ ...lUnsettableParametersIntegrationTests.java | 174 ++++++ ...ormTransactionManagerIntegrationTests.java | 50 ++ .../data/couchbase/transactions/ObjectId.java | 14 + .../couchbase/transactions/PersonService.java | 179 +++++++ .../transactions/PersonServiceReactive.java | 93 ++++ ...TransactionalTemplateIntegrationTests.java | 206 +++++++ .../transactions/ReplaceLoopThread.java | 68 +++ .../SimulateFailureException.java | 15 + .../TransactionTemplateIntegrationTests.java | 381 +++++++++++++ .../transactions/TransactionsConfig.java | 46 ++ ...onAllowableOperationsIntegrationTests.java | 131 +++++ ...iveTransactionsPersonIntegrationTests.java | 264 +++++++++ ...eTransactionsTemplateIntegrationTests.java | 355 ++++++++++++ ...onAllowableOperationsIntegrationTests.java | 132 +++++ ...KTransactionsTemplateIntegrationTests.java | 422 +++++++++++++++ .../util/TransactionTestUtil.java | 38 ++ .../util/ClusterAwareIntegrationTests.java | 16 +- .../couchbase/util/JavaIntegrationTests.java | 100 +++- .../data/couchbase/util/TestCluster.java | 4 +- .../couchbase/util/TestClusterConfig.java | 1 + .../data/couchbase/util/Util.java | 21 + src/test/resources/integration.properties | 2 +- src/test/resources/logback.xml | 2 + 136 files changed, 8634 insertions(+), 1287 deletions(-) delete mode 100755 .mvn/wrapper/maven-wrapper.jar create mode 100644 src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithCas.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/PersonService.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index 01e67997377a393fd672c7dcde9dccbedf0cb1e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48337 zcmbTe1CV9Qwl>;j+wQV$+qSXFw%KK)%eHN!%U!l@+x~l>b1vR}@9y}|TM-#CBjy|< zb7YRpp)Z$$Gzci_H%LgxZ{NNV{%Qa9gZlF*E2<($D=8;N5Asbx8se{Sz5)O13x)rc z5cR(k$_mO!iis+#(8-D=#R@|AF(8UQ`L7dVNSKQ%v^P|1A%aF~Lye$@HcO@sMYOb3 zl`5!ThJ1xSJwsg7hVYFtE5vS^5UE0$iDGCS{}RO;R#3y#{w-1hVSg*f1)7^vfkxrm!!N|oTR0Hj?N~IbVk+yC#NK} z5myv()UMzV^!zkX@O=Yf!(Z_bF7}W>k*U4@--&RH0tHiHY0IpeezqrF#@8{E$9d=- z7^kT=1Bl;(Q0k{*_vzz1Et{+*lbz%mkIOw(UA8)EE-Pkp{JtJhe@VXQ8sPNTn$Vkj zicVp)sV%0omhsj;NCmI0l8zzAipDV#tp(Jr7p_BlL$}Pys_SoljztS%G-Wg+t z&Q#=<03Hoga0R1&L!B);r{Cf~b$G5p#@?R-NNXMS8@cTWE^7V!?ixz(Ag>lld;>COenWc$RZ61W+pOW0wh>sN{~j; zCBj!2nn|4~COwSgXHFH?BDr8pK323zvmDK-84ESq25b;Tg%9(%NneBcs3;r znZpzntG%E^XsSh|md^r-k0Oen5qE@awGLfpg;8P@a-s<{Fwf?w3WapWe|b-CQkqlo z46GmTdPtkGYdI$e(d9Zl=?TU&uv94VR`g|=7xB2Ur%=6id&R2 z4e@fP7`y58O2sl;YBCQFu7>0(lVt-r$9|06Q5V>4=>ycnT}Fyz#9p;3?86`ZD23@7 z7n&`!LXzjxyg*P4Tz`>WVvpU9-<5MDSDcb1 zZaUyN@7mKLEPGS$^odZcW=GLe?3E$JsMR0kcL4#Z=b4P94Q#7O%_60{h>0D(6P*VH z3}>$stt2s!)w4C4 z{zsj!EyQm$2ARSHiRm49r7u)59ZyE}ZznFE7AdF&O&!-&(y=?-7$LWcn4L_Yj%w`qzwz`cLqPRem1zN; z)r)07;JFTnPODe09Z)SF5@^uRuGP~Mjil??oWmJTaCb;yx4?T?d**;AW!pOC^@GnT zaY`WF609J>fG+h?5&#}OD1<%&;_lzM2vw70FNwn2U`-jMH7bJxdQM#6+dPNiiRFGT z7zc{F6bo_V%NILyM?rBnNsH2>Bx~zj)pJ}*FJxW^DC2NLlOI~18Mk`7sl=t`)To6Ui zu4GK6KJx^6Ms4PP?jTn~jW6TOFLl3e2-q&ftT=31P1~a1%7=1XB z+H~<1dh6%L)PbBmtsAr38>m~)?k3}<->1Bs+;227M@?!S+%X&M49o_e)X8|vZiLVa z;zWb1gYokP;Sbao^qD+2ZD_kUn=m=d{Q9_kpGxcbdQ0d5<_OZJ!bZJcmgBRf z!Cdh`qQ_1NLhCulgn{V`C%|wLE8E6vq1Ogm`wb;7Dj+xpwik~?kEzDT$LS?#%!@_{ zhOoXOC95lVcQU^pK5x$Da$TscVXo19Pps zA!(Mk>N|tskqBn=a#aDC4K%jV#+qI$$dPOK6;fPO)0$0j$`OV+mWhE+TqJoF5dgA=TH-}5DH_)H_ zh?b(tUu@65G-O)1ah%|CsU8>cLEy0!Y~#ut#Q|UT92MZok0b4V1INUL-)Dvvq`RZ4 zTU)YVX^r%_lXpn_cwv`H=y49?!m{krF3Rh7O z^z7l4D<+^7E?ji(L5CptsPGttD+Z7{N6c-`0V^lfFjsdO{aJMFfLG9+wClt<=Rj&G zf6NgsPSKMrK6@Kvgarmx{&S48uc+ZLIvk0fbH}q-HQ4FSR33$+%FvNEusl6xin!?e z@rrWUP5U?MbBDeYSO~L;S$hjxISwLr&0BOSd?fOyeCWm6hD~)|_9#jo+PVbAY3wzf zcZS*2pX+8EHD~LdAl>sA*P>`g>>+&B{l94LNLp#KmC)t6`EPhL95s&MMph46Sk^9x%B$RK!2MI--j8nvN31MNLAJBsG`+WMvo1}xpaoq z%+W95_I`J1Pr&Xj`=)eN9!Yt?LWKs3-`7nf)`G6#6#f+=JK!v943*F&veRQxKy-dm(VcnmA?K_l~ zfDWPYl6hhN?17d~^6Zuo@>Hswhq@HrQ)sb7KK^TRhaM2f&td)$6zOn7we@ zd)x4-`?!qzTGDNS-E(^mjM%d46n>vPeMa;%7IJDT(nC)T+WM5F-M$|p(78W!^ck6)A_!6|1o!D97tw8k|5@0(!8W&q9*ovYl)afk z2mxnniCOSh7yHcSoEu8k`i15#oOi^O>uO_oMpT=KQx4Ou{&C4vqZG}YD0q!{RX=`#5wmcHT=hqW3;Yvg5Y^^ ziVunz9V)>2&b^rI{ssTPx26OxTuCw|+{tt_M0TqD?Bg7cWN4 z%UH{38(EW1L^!b~rtWl)#i}=8IUa_oU8**_UEIw+SYMekH;Epx*SA7Hf!EN&t!)zuUca@_Q^zW(u_iK_ zrSw{nva4E6-Npy9?lHAa;b(O z`I74A{jNEXj(#r|eS^Vfj-I!aHv{fEkzv4=F%z0m;3^PXa27k0Hq#RN@J7TwQT4u7 ztisbp3w6#k!RC~!5g-RyjpTth$lf!5HIY_5pfZ8k#q!=q*n>~@93dD|V>=GvH^`zn zVNwT@LfA8^4rpWz%FqcmzX2qEAhQ|_#u}md1$6G9qD%FXLw;fWWvqudd_m+PzI~g3 z`#WPz`M1XUKfT3&T4~XkUie-C#E`GN#P~S(Zx9%CY?EC?KP5KNK`aLlI1;pJvq@d z&0wI|dx##t6Gut6%Y9c-L|+kMov(7Oay++QemvI`JOle{8iE|2kZb=4x%a32?>-B~ z-%W$0t&=mr+WJ3o8d(|^209BapD`@6IMLbcBlWZlrr*Yrn^uRC1(}BGNr!ct z>xzEMV(&;ExHj5cce`pk%6!Xu=)QWtx2gfrAkJY@AZlHWiEe%^_}mdzvs(6>k7$e; ze4i;rv$_Z$K>1Yo9f4&Jbx80?@X!+S{&QwA3j#sAA4U4#v zwZqJ8%l~t7V+~BT%j4Bwga#Aq0&#rBl6p$QFqS{DalLd~MNR8Fru+cdoQ78Dl^K}@l#pmH1-e3?_0tZKdj@d2qu z_{-B11*iuywLJgGUUxI|aen-((KcAZZdu8685Zi1b(#@_pmyAwTr?}#O7zNB7U6P3 zD=_g*ZqJkg_9_X3lStTA-ENl1r>Q?p$X{6wU6~e7OKNIX_l9T# z>XS?PlNEM>P&ycY3sbivwJYAqbQH^)z@PobVRER*Ud*bUi-hjADId`5WqlZ&o+^x= z-Lf_80rC9>tqFBF%x#`o>69>D5f5Kp->>YPi5ArvgDwV#I6!UoP_F0YtfKoF2YduA zCU!1`EB5;r68;WyeL-;(1K2!9sP)at9C?$hhy(dfKKBf}>skPqvcRl>UTAB05SRW! z;`}sPVFFZ4I%YrPEtEsF(|F8gnfGkXI-2DLsj4_>%$_ZX8zVPrO=_$7412)Mr9BH{ zwKD;e13jP2XK&EpbhD-|`T~aI`N(*}*@yeDUr^;-J_`fl*NTSNbupyHLxMxjwmbuw zt3@H|(hvcRldE+OHGL1Y;jtBN76Ioxm@UF1K}DPbgzf_a{`ohXp_u4=ps@x-6-ZT>F z)dU`Jpu~Xn&Qkq2kg%VsM?mKC)ArP5c%r8m4aLqimgTK$atIxt^b8lDVPEGDOJu!) z%rvASo5|v`u_}vleP#wyu1$L5Ta%9YOyS5;w2I!UG&nG0t2YL|DWxr#T7P#Ww8MXDg;-gr`x1?|V`wy&0vm z=hqozzA!zqjOm~*DSI9jk8(9nc4^PL6VOS$?&^!o^Td8z0|eU$9x8s{8H!9zK|)NO zqvK*dKfzG^Dy^vkZU|p9c+uVV3>esY)8SU1v4o{dZ+dPP$OT@XCB&@GJ<5U&$Pw#iQ9qzuc`I_%uT@%-v zLf|?9w=mc;b0G%%{o==Z7AIn{nHk`>(!e(QG%(DN75xfc#H&S)DzSFB6`J(cH!@mX3mv_!BJv?ByIN%r-i{Y zBJU)}Vhu)6oGoQjT2tw&tt4n=9=S*nQV`D_MSw7V8u1-$TE>F-R6Vo0giKnEc4NYZ zAk2$+Tba~}N0wG{$_7eaoCeb*Ubc0 zq~id50^$U>WZjmcnIgsDione)f+T)0ID$xtgM zpGZXmVez0DN!)ioW1E45{!`G9^Y1P1oXhP^rc@c?o+c$^Kj_bn(Uo1H2$|g7=92v- z%Syv9Vo3VcibvH)b78USOTwIh{3%;3skO_htlfS?Cluwe`p&TMwo_WK6Z3Tz#nOoy z_E17(!pJ>`C2KECOo38F1uP0hqBr>%E=LCCCG{j6$b?;r?Fd$4@V-qjEzgWvzbQN%_nlBg?Ly`x-BzO2Nnd1 zuO|li(oo^Rubh?@$q8RVYn*aLnlWO_dhx8y(qzXN6~j>}-^Cuq4>=d|I>vhcjzhSO zU`lu_UZ?JaNs1nH$I1Ww+NJI32^qUikAUfz&k!gM&E_L=e_9}!<(?BfH~aCmI&hfzHi1~ zraRkci>zMPLkad=A&NEnVtQQ#YO8Xh&K*;6pMm$ap_38m;XQej5zEqUr`HdP&cf0i z5DX_c86@15jlm*F}u-+a*^v%u_hpzwN2eT66Zj_1w)UdPz*jI|fJb#kSD_8Q-7q9gf}zNu2h=q{)O*XH8FU)l|m;I;rV^QpXRvMJ|7% zWKTBX*cn`VY6k>mS#cq!uNw7H=GW3?wM$8@odjh$ynPiV7=Ownp}-|fhULZ)5{Z!Q z20oT!6BZTK;-zh=i~RQ$Jw>BTA=T(J)WdnTObDM#61lUm>IFRy@QJ3RBZr)A9CN!T z4k7%)I4yZ-0_n5d083t!=YcpSJ}M5E8`{uIs3L0lIaQws1l2}+w2(}hW&evDlMnC!WV?9U^YXF}!N*iyBGyCyJ<(2(Ca<>!$rID`( zR?V~-53&$6%DhW=)Hbd-oetTXJ-&XykowOx61}1f`V?LF=n8Nb-RLFGqheS7zNM_0 z1ozNap9J4GIM1CHj-%chrCdqPlP307wfrr^=XciOqn?YPL1|ozZ#LNj8QoCtAzY^q z7&b^^K&?fNSWD@*`&I+`l9 zP2SlD0IO?MK60nbucIQWgz85l#+*<{*SKk1K~|x{ux+hn=SvE_XE`oFlr7$oHt-&7 zP{+x)*y}Hnt?WKs_Ymf(J^aoe2(wsMMRPu>Pg8H#x|zQ_=(G5&ieVhvjEXHg1zY?U zW-hcH!DJPr+6Xnt)MslitmnHN(Kgs4)Y`PFcV0Qvemj;GG`kf<>?p})@kd9DA7dqs zNtGRKVr0%x#Yo*lXN+vT;TC{MR}}4JvUHJHDLd-g88unUj1(#7CM<%r!Z1Ve>DD)FneZ| z8Q0yI@i4asJaJ^ge%JPl>zC3+UZ;UDUr7JvUYNMf=M2t{It56OW1nw#K8%sXdX$Yg zpw3T=n}Om?j3-7lu)^XfBQkoaZ(qF0D=Aw&D%-bsox~`8Y|!whzpd5JZ{dmM^A5)M zOwWEM>bj}~885z9bo{kWFA0H(hv(vL$G2;pF$@_M%DSH#g%V*R(>;7Z7eKX&AQv1~ z+lKq=488TbTwA!VtgSHwduwAkGycunrg}>6oiX~;Kv@cZlz=E}POn%BWt{EEd;*GV zmc%PiT~k<(TA`J$#6HVg2HzF6Iw5w9{C63y`Y7?OB$WsC$~6WMm3`UHaWRZLN3nKiV# zE;iiu_)wTr7ZiELH$M^!i5eC9aRU#-RYZhCl1z_aNs@f`tD4A^$xd7I_ijCgI!$+| zsulIT$KB&PZ}T-G;Ibh@UPafvOc-=p7{H-~P)s{3M+;PmXe7}}&Mn+9WT#(Jmt5DW%73OBA$tC#Ug!j1BR~=Xbnaz4hGq zUOjC*z3mKNbrJm1Q!Ft^5{Nd54Q-O7<;n})TTQeLDY3C}RBGwhy*&wgnl8dB4lwkG zBX6Xn#hn|!v7fp@@tj9mUPrdD!9B;tJh8-$aE^t26n_<4^=u~s_MfbD?lHnSd^FGGL6the7a|AbltRGhfET*X;P7=AL?WPjBtt;3IXgUHLFMRBz(aWW_ zZ?%%SEPFu&+O?{JgTNB6^5nR@)rL6DFqK$KS$bvE#&hrPs>sYsW=?XzOyD6ixglJ8rdt{P8 zPAa*+qKt(%ju&jDkbB6x7aE(={xIb*&l=GF(yEnWPj)><_8U5m#gQIIa@l49W_=Qn^RCsYqlEy6Om%!&e~6mCAfDgeXe3aYpHQAA!N|kmIW~Rk}+p6B2U5@|1@7iVbm5&e7E3;c9q@XQlb^JS(gmJl%j9!N|eNQ$*OZf`3!;raRLJ z;X-h>nvB=S?mG!-VH{65kwX-UwNRMQB9S3ZRf`hL z#WR)+rn4C(AG(T*FU}`&UJOU4#wT&oDyZfHP^s9#>V@ens??pxuu-6RCk=Er`DF)X z>yH=P9RtrtY;2|Zg3Tnx3Vb!(lRLedVRmK##_#;Kjnlwq)eTbsY8|D{@Pjn_=kGYO zJq0T<_b;aB37{U`5g6OSG=>|pkj&PohM%*O#>kCPGK2{0*=m(-gKBEOh`fFa6*~Z! zVxw@7BS%e?cV^8{a`Ys4;w=tH4&0izFxgqjE#}UfsE^?w)cYEQjlU|uuv6{>nFTp| zNLjRRT1{g{?U2b6C^w{!s+LQ(n}FfQPDfYPsNV?KH_1HgscqG7z&n3Bh|xNYW4i5i zT4Uv-&mXciu3ej=+4X9h2uBW9o(SF*N~%4%=g|48R-~N32QNq!*{M4~Y!cS4+N=Zr z?32_`YpAeg5&r_hdhJkI4|i(-&BxCKru`zm9`v+CN8p3r9P_RHfr{U$H~RddyZKw{ zR?g5i>ad^Ge&h?LHlP7l%4uvOv_n&WGc$vhn}2d!xIWrPV|%x#2Q-cCbQqQ|-yoTe z_C(P))5e*WtmpB`Fa~#b*yl#vL4D_h;CidEbI9tsE%+{-4ZLKh#9^{mvY24#u}S6oiUr8b0xLYaga!(Fe7Dxi}v6 z%5xNDa~i%tN`Cy_6jbk@aMaY(xO2#vWZh9U?mrNrLs5-*n>04(-Dlp%6AXsy;f|a+ z^g~X2LhLA>xy(8aNL9U2wr=ec%;J2hEyOkL*D%t4cNg7WZF@m?kF5YGvCy`L5jus# zGP8@iGTY|ov#t&F$%gkWDoMR7v*UezIWMeg$C2~WE9*5%}$3!eFiFJ?hypfIA(PQT@=B|^Ipcu z{9cM3?rPF|gM~{G)j*af1hm+l92W7HRpQ*hSMDbh(auwr}VBG7`ldp>`FZ^amvau zTa~Y7%tH@>|BB6kSRGiWZFK?MIzxEHKGz#P!>rB-90Q_UsZ=uW6aTzxY{MPP@1rw- z&RP^Ld%HTo($y?6*aNMz8h&E?_PiO{jq%u4kr#*uN&Q+Yg1Rn831U4A6u#XOzaSL4 zrcM+0v@%On8N*Mj!)&IzXW6A80bUK&3w|z06cP!UD^?_rb_(L-u$m+#%YilEjkrlxthGCLQ@Q?J!p?ggv~0 z!qipxy&`w48T0(Elsz<^hp_^#1O1cNJ1UG=61Nc=)rlRo_P6v&&h??Qvv$ifC3oJh zo)ZZhU5enAqU%YB>+FU!1vW)i$m-Z%w!c&92M1?))n4z1a#4-FufZ$DatpJ^q)_Zif z;Br{HmZ|8LYRTi`#?TUfd;#>c4@2qM5_(H+Clt@kkQT+kx78KACyvY)?^zhyuN_Z& z-*9_o_f3IC2lX^(aLeqv#>qnelb6_jk+lgQh;TN>+6AU9*6O2h_*=74m;xSPD1^C9 zE0#!+B;utJ@8P6_DKTQ9kNOf`C*Jj0QAzsngKMQVDUsp=k~hd@wt}f{@$O*xI!a?p z6Gti>uE}IKAaQwKHRb0DjmhaF#+{9*=*^0)M-~6lPS-kCI#RFGJ-GyaQ+rhbmhQef zwco))WNA1LFr|J3Qsp4ra=_j?Y%b{JWMX6Zr`$;*V`l`g7P0sP?Y1yOY;e0Sb!AOW0Em=U8&i8EKxTd$dX6=^Iq5ZC%zMT5Jjj%0_ zbf|}I=pWjBKAx7wY<4-4o&E6vVStcNlT?I18f5TYP9!s|5yQ_C!MNnRyDt7~u~^VS@kKd}Zwc~? z=_;2}`Zl^xl3f?ce8$}g^V)`b8Pz88=9FwYuK_x%R?sbAF-dw`*@wokEC3mp0Id>P z>OpMGxtx!um8@gW2#5|)RHpRez+)}_p;`+|*m&3&qy{b@X>uphcgAVgWy`?Nc|NlH z75_k2%3h7Fy~EkO{vBMuzV7lj4B}*1Cj(Ew7oltspA6`d69P`q#Y+rHr5-m5&be&( zS1GcP5u#aM9V{fUQTfHSYU`kW&Wsxeg;S*{H_CdZ$?N>S$JPv!_6T(NqYPaS{yp0H7F~7vy#>UHJr^lV?=^vt4?8$v8vkI-1eJ4{iZ!7D5A zg_!ZxZV+9Wx5EIZ1%rbg8`-m|=>knmTE1cpaBVew_iZpC1>d>qd3`b6<(-)mtJBmd zjuq-qIxyKvIs!w4$qpl{0cp^-oq<=-IDEYV7{pvfBM7tU+ zfX3fc+VGtqjPIIx`^I0i>*L-NfY=gFS+|sC75Cg;2<)!Y`&p&-AxfOHVADHSv1?7t zlOKyXxi|7HdwG5s4T0))dWudvz8SZpxd<{z&rT<34l}XaaP86x)Q=2u5}1@Sgc41D z2gF)|aD7}UVy)bnm788oYp}Es!?|j73=tU<_+A4s5&it~_K4 z;^$i0Vnz8y&I!abOkzN|Vz;kUTya#Wi07>}Xf^7joZMiHH3Mdy@e_7t?l8^A!r#jTBau^wn#{|!tTg=w01EQUKJOca!I zV*>St2399#)bMF++1qS8T2iO3^oA`i^Px*i)T_=j=H^Kp4$Zao(>Y)kpZ=l#dSgcUqY=7QbGz9mP9lHnII8vl?yY9rU+i%X)-j0&-- zrtaJsbkQ$;DXyIqDqqq)LIJQ!`MIsI;goVbW}73clAjN;1Rtp7%{67uAfFNe_hyk= zn=8Q1x*zHR?txU)x9$nQu~nq7{Gbh7?tbgJ>i8%QX3Y8%T{^58W^{}(!9oPOM+zF3 zW`%<~q@W}9hoes56uZnNdLkgtcRqPQ%W8>o7mS(j5Sq_nN=b0A`Hr%13P{uvH?25L zMfC&Z0!{JBGiKoVwcIhbbx{I35o}twdI_ckbs%1%AQ(Tdb~Xw+sXAYcOoH_9WS(yM z2dIzNLy4D%le8Fxa31fd;5SuW?ERAsagZVEo^i};yjBhbxy9&*XChFtOPV8G77{8! zlYemh2vp7aBDMGT;YO#=YltE~(Qv~e7c=6$VKOxHwvrehtq>n|w}vY*YvXB%a58}n zqEBR4zueP@A~uQ2x~W-{o3|-xS@o>Ad@W99)ya--dRx;TZLL?5E(xstg(6SwDIpL5 zMZ)+)+&(hYL(--dxIKB*#v4mDq=0ve zNU~~jk426bXlS8%lcqsvuqbpgn zbFgxap;17;@xVh+Y~9@+-lX@LQv^Mw=yCM&2!%VCfZsiwN>DI=O?vHupbv9!4d*>K zcj@a5vqjcjpwkm@!2dxzzJGQ7#ujW(IndUuYC)i3N2<*doRGX8a$bSbyRO#0rA zUpFyEGx4S9$TKuP9BybRtjcAn$bGH-9>e(V{pKYPM3waYrihBCQf+UmIC#E=9v?or z_7*yzZfT|)8R6>s(lv6uzosT%WoR`bQIv(?llcH2Bd@26?zU%r1K25qscRrE1 z9TIIP_?`78@uJ{%I|_K;*syVinV;pCW!+zY-!^#n{3It^6EKw{~WIA0pf_hVzEZy zFzE=d-NC#mge{4Fn}we02-%Zh$JHKpXX3qF<#8__*I}+)Npxm?26dgldWyCmtwr9c zOXI|P0zCzn8M_Auv*h9;2lG}x*E|u2!*-s}moqS%Z`?O$<0amJG9n`dOV4**mypG- zE}In1pOQ|;@@Jm;I#m}jkQegIXag4K%J;C7<@R2X8IdsCNqrbsaUZZRT|#6=N!~H} zlc2hPngy9r+Gm_%tr9V&HetvI#QwUBKV&6NC~PK>HNQ3@fHz;J&rR7XB>sWkXKp%A ziLlogA`I*$Z7KzLaX^H_j)6R|9Q>IHc? z{s0MsOW>%xW|JW=RUxY@@0!toq`QXa=`j;)o2iDBiDZ7c4Bc>BiDTw+zk}Jm&vvH8qX$R`M6Owo>m%n`eizBf!&9X6 z)f{GpMak@NWF+HNg*t#H5yift5@QhoYgT7)jxvl&O=U54Z>FxT5prvlDER}AwrK4Q z*&JP9^k332OxC$(E6^H`#zw|K#cpwy0i*+!z{T23;dqUKbjP!-r*@_!sp+Uec@^f0 zIJMjqhp?A#YoX5EB%iWu;mxJ1&W6Nb4QQ@GElqNjFNRc*=@aGc$PHdoUptckkoOZC zk@c9i+WVnDI=GZ1?lKjobDl%nY2vW~d)eS6Lch&J zDi~}*fzj9#<%xg<5z-4(c}V4*pj~1z2z60gZc}sAmys^yvobWz)DKDGWuVpp^4-(!2Nn7 z3pO})bO)({KboXlQA>3PIlg@Ie$a=G;MzVeft@OMcKEjIr=?;=G0AH?dE_DcNo%n$_bFjqQ8GjeIyJP^NkX~7e&@+PqnU-c3@ABap z=}IZvC0N{@fMDOpatOp*LZ7J6Hz@XnJzD!Yh|S8p2O($2>A4hbpW{8?#WM`uJG>?} zwkDF3dimqejl$3uYoE7&pr5^f4QP-5TvJ;5^M?ZeJM8ywZ#Dm`kR)tpYieQU;t2S! z05~aeOBqKMb+`vZ2zfR*2(&z`Y1VROAcR(^Q7ZyYlFCLHSrTOQm;pnhf3Y@WW#gC1 z7b$_W*ia0@2grK??$pMHK>a$;J)xIx&fALD4)w=xlT=EzrwD!)1g$2q zy8GQ+r8N@?^_tuCKVi*q_G*!#NxxY#hpaV~hF} zF1xXy#XS|q#)`SMAA|46+UnJZ__lETDwy}uecTSfz69@YO)u&QORO~F^>^^j-6q?V z-WK*o?XSw~ukjoIT9p6$6*OStr`=+;HrF#)p>*>e|gy0D9G z#TN(VSC11^F}H#?^|^ona|%;xCC!~H3~+a>vjyRC5MPGxFqkj6 zttv9I_fv+5$vWl2r8+pXP&^yudvLxP44;9XzUr&a$&`?VNhU^$J z`3m68BAuA?ia*IF%Hs)@>xre4W0YoB^(X8RwlZ?pKR)rvGX?u&K`kb8XBs^pe}2v* z_NS*z7;4%Be$ts_emapc#zKjVMEqn8;aCX=dISG3zvJP>l4zHdpUwARLixQSFzLZ0 z$$Q+9fAnVjA?7PqANPiH*XH~VhrVfW11#NkAKjfjQN-UNz?ZT}SG#*sk*)VUXZ1$P zdxiM@I2RI7Tr043ZgWd3G^k56$Non@LKE|zLwBgXW#e~{7C{iB3&UjhKZPEj#)cH9 z%HUDubc0u@}dBz>4zU;sTluxBtCl!O4>g9ywc zhEiM-!|!C&LMjMNs6dr6Q!h{nvTrNN0hJ+w*h+EfxW=ro zxAB%*!~&)uaqXyuh~O`J(6e!YsD0o0l_ung1rCAZt~%4R{#izD2jT~${>f}m{O!i4 z`#UGbiSh{L=FR`Q`e~9wrKHSj?I>eXHduB`;%TcCTYNG<)l@A%*Ld?PK=fJi}J? z9T-|Ib8*rLE)v_3|1+Hqa!0ch>f% zfNFz@o6r5S`QQJCwRa4zgx$7AyQ7ZTv2EM7ZQHh!72CFL+qT`Y)k!)|Zr;7mcfV8T z)PB$1r*5rUzgE@y^E_kDG3Ol5n6q}eU2hJcXY7PI1}N=>nwC6k%nqxBIAx4Eix*`W zch0}3aPFe5*lg1P(=7J^0ZXvpOi9v2l*b?j>dI%iamGp$SmFaxpZod*TgYiyhF0= za44lXRu%9MA~QWN;YX@8LM32BqKs&W4&a3ve9C~ndQq>S{zjRNj9&&8k-?>si8)^m zW%~)EU)*$2YJzTXjRV=-dPAu;;n2EDYb=6XFyz`D0f2#29(mUX}*5~KU3k>$LwN#OvBx@ zl6lC>UnN#0?mK9*+*DMiboas!mmGnoG%gSYeThXI<=rE(!Pf-}oW}?yDY0804dH3o zo;RMFJzxP|srP-6ZmZ_peiVycfvH<`WJa9R`Z#suW3KrI*>cECF(_CB({ToWXSS18#3%vihZZJ{BwJPa?m^(6xyd1(oidUkrOU zlqyRQUbb@W_C)5Q)%5bT3K0l)w(2cJ-%?R>wK35XNl&}JR&Pn*laf1M#|s4yVXQS# zJvkT$HR;^3k{6C{E+{`)J+~=mPA%lv1T|r#kN8kZP}os;n39exCXz^cc{AN(Ksc%} zA561&OeQU8gIQ5U&Y;Ca1TatzG`K6*`9LV<|GL-^=qg+nOx~6 zBEMIM7Q^rkuhMtw(CZtpU(%JlBeV?KC+kjVDL34GG1sac&6(XN>nd+@Loqjo%i6I~ zjNKFm^n}K=`z8EugP20fd_%~$Nfu(J(sLL1gvXhxZt|uvibd6rLXvM%!s2{g0oNA8 z#Q~RfoW8T?HE{ge3W>L9bx1s2_L83Odx)u1XUo<`?a~V-_ZlCeB=N-RWHfs1(Yj!_ zP@oxCRysp9H8Yy@6qIc69TQx(1P`{iCh)8_kH)_vw1=*5JXLD(njxE?2vkOJ z>qQz!*r`>X!I69i#1ogdVVB=TB40sVHX;gak=fu27xf*}n^d>@*f~qbtVMEW!_|+2 zXS`-E%v`_>(m2sQnc6+OA3R z-6K{6$KZsM+lF&sn~w4u_md6J#+FzqmtncY;_ z-Q^D=%LVM{A0@VCf zV9;?kF?vV}*=N@FgqC>n-QhKJD+IT7J!6llTEH2nmUxKiBa*DO4&PD5=HwuD$aa(1 z+uGf}UT40OZAH@$jjWoI7FjOQAGX6roHvf_wiFKBfe4w|YV{V;le}#aT3_Bh^$`Pp zJZGM_()iFy#@8I^t{ryOKQLt%kF7xq&ZeD$$ghlTh@bLMv~||?Z$#B2_A4M&8)PT{ zyq$BzJpRrj+=?F}zH+8XcPvhRP+a(nnX2^#LbZqgWQ7uydmIM&FlXNx4o6m;Q5}rB z^ryM&o|~a-Zb20>UCfSFwdK4zfk$*~<|90v0=^!I?JnHBE{N}74iN;w6XS=#79G+P zB|iewe$kk;9^4LinO>)~KIT%%4Io6iFFXV9gJcIvu-(!um{WfKAwZDmTrv=wb#|71 zWqRjN8{3cRq4Ha2r5{tw^S>0DhaC3m!i}tk9q08o>6PtUx1GsUd{Z17FH45rIoS+oym1>3S0B`>;uo``+ADrd_Um+8s$8V6tKsA8KhAm z{pTv@zj~@+{~g&ewEBD3um9@q!23V_8Nb0_R#1jcg0|MyU)?7ua~tEY63XSvqwD`D zJ+qY0Wia^BxCtXpB)X6htj~*7)%un+HYgSsSJPAFED7*WdtlFhuJj5d3!h8gt6$(s ztrx=0hFH8z(Fi9}=kvPI?07j&KTkssT=Vk!d{-M50r!TsMD8fPqhN&%(m5LGpO>}L zse;sGl_>63FJ)(8&8(7Wo2&|~G!Lr^cc!uuUBxGZE)ac7Jtww7euxPo)MvxLXQXlk zeE>E*nMqAPwW0&r3*!o`S7wK&078Q#1bh!hNbAw0MFnK-2gU25&8R@@j5}^5-kHeR z!%krca(JG%&qL2mjFv380Gvb*eTLllTaIpVr3$gLH2e3^xo z=qXjG0VmES%OXAIsOQG|>{aj3fv+ZWdoo+a9tu8)4AyntBP>+}5VEmv@WtpTo<-aH zF4C(M#dL)MyZmU3sl*=TpAqU#r>c8f?-zWMq`wjEcp^jG2H`8m$p-%TW?n#E5#Th+ z7Zy#D>PPOA4|G@-I$!#Yees_9Ku{i_Y%GQyM)_*u^nl+bXMH!f_ z8>BM|OTex;vYWu`AhgfXFn)0~--Z7E0WR-v|n$XB-NOvjM156WR(eu z(qKJvJ%0n+%+%YQP=2Iz-hkgI_R>7+=)#FWjM#M~Y1xM8m_t8%=FxV~Np$BJ{^rg9 z5(BOvYfIY{$h1+IJyz-h`@jhU1g^Mo4K`vQvR<3wrynWD>p{*S!kre-(MT&`7-WK! zS}2ceK+{KF1yY*x7FH&E-1^8b$zrD~Ny9|9(!1Y)a#)*zf^Uo@gy~#%+*u`U!R`^v zCJ#N!^*u_gFq7;-XIYKXvac$_=booOzPgrMBkonnn%@#{srUC<((e*&7@YR?`CP;o zD2*OE0c%EsrI72QiN`3FpJ#^Bgf2~qOa#PHVmbzonW=dcrs92>6#{pEnw19AWk%;H zJ4uqiD-dx*w2pHf8&Jy{NXvGF^Gg!ungr2StHpMQK5^+ zEmDjjBonrrT?d9X;BHSJeU@lX19|?On)(Lz2y-_;_!|}QQMsq4Ww9SmzGkzVPQTr* z)YN>_8i^rTM>Bz@%!!v)UsF&Nb{Abz>`1msFHcf{)Ufc_a-mYUPo@ei#*%I_jWm#7 zX01=Jo<@6tl`c;P_uri^gJxDVHOpCano2Xc5jJE8(;r@y6THDE>x*#-hSKuMQ_@nc z68-JLZyag_BTRE(B)Pw{B;L0+Zx!5jf%z-Zqug*og@^ zs{y3{Za(0ywO6zYvES>SW*cd4gwCN^o9KQYF)Lm^hzr$w&spGNah6g>EQBufQCN!y zI5WH$K#67$+ic{yKAsX@el=SbBcjRId*cs~xk~3BBpQsf%IsoPG)LGs zdK0_rwz7?L0XGC^2$dktLQ9qjwMsc1rpGx2Yt?zmYvUGnURx(1k!kmfPUC@2Pv;r9 z`-Heo+_sn+!QUJTAt;uS_z5SL-GWQc#pe0uA+^MCWH=d~s*h$XtlN)uCI4$KDm4L$ zIBA|m0o6@?%4HtAHRcDwmzd^(5|KwZ89#UKor)8zNI^EsrIk z1QLDBnNU1!PpE3iQg9^HI){x7QXQV{&D>2U%b_II>*2*HF2%>KZ>bxM)Jx4}|CCEa`186nD_B9h`mv6l45vRp*L+z_nx5i#9KvHi>rqxJIjKOeG(5lCeo zLC|-b(JL3YP1Ds=t;U!Y&Gln*Uwc0TnDSZCnh3m$N=xWMcs~&Rb?w}l51ubtz=QUZsWQhWOX;*AYb)o(^<$zU_v=cFwN~ZVrlSLx| zpr)Q7!_v*%U}!@PAnZLqOZ&EbviFbej-GwbeyaTq)HSBB+tLH=-nv1{MJ-rGW%uQ1 znDgP2bU@}!Gd=-;3`KlJYqB@U#Iq8Ynl%eE!9g;d*2|PbC{A}>mgAc8LK<69qcm)piu?`y~3K8zlZ1>~K_4T{%4zJG6H?6%{q3B-}iP_SGXELeSv*bvBq~^&C=3TsP z9{cff4KD2ZYzkArq=;H(Xd)1CAd%byUXZdBHcI*%a24Zj{Hm@XA}wj$=7~$Q*>&4} z2-V62ek{rKhPvvB711`qtAy+q{f1yWuFDcYt}hP)Vd>G?;VTb^P4 z(QDa?zvetCoB_)iGdmQ4VbG@QQ5Zt9a&t(D5Rf#|hC`LrONeUkbV)QF`ySE5x+t_v z-(cW{S13ye9>gtJm6w&>WwJynxJQm8U2My?#>+(|)JK}bEufIYSI5Y}T;vs?rzmLE zAIk%;^qbd@9WUMi*cGCr=oe1-nthYRQlhVHqf{ylD^0S09pI}qOQO=3&dBsD)BWo# z$NE2Ix&L&4|Aj{;ed*A?4z4S!7o_Kg^8@%#ZW26_F<>y4ghZ0b|3+unIoWDUVfen~ z`4`-cD7qxQSm9hF-;6WvCbu$t5r$LCOh}=`k1(W<&bG-xK{VXFl-cD%^Q*x-9eq;k8FzxAqZB zH@ja_3%O7XF~>owf3LSC_Yn!iO}|1Uc5uN{Wr-2lS=7&JlsYSp3IA%=E?H6JNf()z zh>jA>JVsH}VC>3Be>^UXk&3o&rK?eYHgLwE-qCHNJyzDLmg4G(uOFX5g1f(C{>W3u zn~j`zexZ=sawG8W+|SErqc?uEvQP(YT(YF;u%%6r00FP;yQeH)M9l+1Sv^yddvGo- z%>u>5SYyJ|#8_j&%h3#auTJ!4y@yEg<(wp#(~NH zXP7B#sv@cW{D4Iz1&H@5wW(F82?-JmcBt@Gw1}WK+>FRXnX(8vwSeUw{3i%HX6-pvQS-~Omm#x-udgp{=9#!>kDiLwqs_7fYy{H z)jx_^CY?5l9#fR$wukoI>4aETnU>n<$UY!JDlIvEti908)Cl2Ziyjjtv|P&&_8di> z<^amHu|WgwMBKHNZ)t)AHII#SqDIGTAd<(I0Q_LNPk*?UmK>C5=rIN^gs}@65VR*!J{W;wp5|&aF8605*l-Sj zQk+C#V<#;=Sl-)hzre6n0n{}|F=(#JF)X4I4MPhtm~qKeR8qM?a@h!-kKDyUaDrqO z1xstrCRCmDvdIFOQ7I4qesby8`-5Y>t_E1tUTVOPuNA1De9| z8{B0NBp*X2-ons_BNzb*Jk{cAJ(^F}skK~i;p0V(R7PKEV3bB;syZ4(hOw47M*-r8 z3qtuleeteUl$FHL$)LN|q8&e;QUN4(id`Br{rtsjpBdriO}WHLcr<;aqGyJP{&d6? zMKuMeLbc=2X0Q_qvSbl3r?F8A^oWw9Z{5@uQ`ySGm@DUZ=XJ^mKZ-ipJtmiXjcu<%z?Nj%-1QY*O{NfHd z=V}Y(UnK=f?xLb-_~H1b2T&0%O*2Z3bBDf06-nO*q%6uEaLs;=omaux7nqqW%tP$i zoF-PC%pxc(ymH{^MR_aV{@fN@0D1g&zv`1$Pyu3cvdR~(r*3Y%DJ@&EU?EserVEJ` zEprux{EfT+(Uq1m4F?S!TrZ+!AssSdX)fyhyPW6C`}ko~@y#7acRviE(4>moNe$HXzf zY@@fJa~o_r5nTeZ7ceiXI=k=ISkdp1gd1p)J;SlRn^5;rog!MlTr<<6-U9|oboRBN zlG~o*dR;%?9+2=g==&ZK;Cy0pyQFe)x!I!8g6;hGl`{{3q1_UzZy)J@c{lBIEJVZ& z!;q{8h*zI!kzY#RO8z3TNlN$}l;qj10=}du!tIKJs8O+?KMJDoZ+y)Iu`x`yJ@krO zwxETN$i!bz8{!>BKqHpPha{96eriM?mST)_9Aw-1X^7&;Bf=c^?17k)5&s08^E$m^ zRt02U_r!99xfiow-XC~Eo|Yt8t>32z=rv$Z;Ps|^26H73JS1Xle?;-nisDq$K5G3y znR|l8@rlvv^wj%tdgw+}@F#Ju{SkrQdqZ?5zh;}|IPIdhy3ivi0Q41C@4934naAaY z%+otS8%Muvrr{S-Y96G?b2j0ldu1&coOqsq^vfcUT3}#+=#;fii6@M+hDp}dr9A0Y zjbhvqmB03%4jhsZ{_KQfGh5HKm-=dFxN;3tnwBej^uzcVLrrs z>eFP-jb#~LE$qTP9JJ;#$nVOw%&;}y>ezA6&i8S^7YK#w&t4!A36Ub|or)MJT z^GGrzgcnQf6D+!rtfuX|Pna`Kq*ScO#H=de2B7%;t+Ij<>N5@(Psw%>nT4cW338WJ z>TNgQ^!285hS1JoHJcBk;3I8%#(jBmcpEkHkQDk%!4ygr;Q2a%0T==W zT#dDH>hxQx2E8+jE~jFY$FligkN&{vUZeIn*#I_Ca!l&;yf){eghi z>&?fXc-C$z8ab$IYS`7g!2#!3F@!)cUquAGR2oiR0~1pO<$3Y$B_@S2dFwu~B0e4D z6(WiE@O{(!vP<(t{p|S5#r$jl6h;3@+ygrPg|bBDjKgil!@Sq)5;rXNjv#2)N5_nn zuqEURL>(itBYrT&3mu-|q;soBd52?jMT75cvXYR!uFuVP`QMot+Yq?CO%D9$Jv24r zhq1Q5`FD$r9%&}9VlYcqNiw2#=3dZsho0cKKkv$%X&gmVuv&S__zyz@0zmZdZI59~s)1xFs~kZS0C^271hR*O z9nt$5=y0gjEI#S-iV0paHx!|MUNUq&$*zi>DGt<#?;y;Gms|dS{2#wF-S`G3$^$7g z1#@7C65g$=4Ij?|Oz?X4=zF=QfixmicIw{0oDL5N7iY}Q-vcVXdyQNMb>o_?3A?e6 z$4`S_=6ZUf&KbMgpn6Zt>6n~)zxI1>{HSge3uKBiN$01WB9OXscO?jd!)`?y5#%yp zJvgJU0h+|^MdA{!g@E=dJuyHPOh}i&alC+cY*I3rjB<~DgE{`p(FdHuXW;p$a+%5` zo{}x#Ex3{Sp-PPi)N8jGVo{K!$^;z%tVWm?b^oG8M?Djk)L)c{_-`@F|8LNu|BTUp zQY6QJVzVg8S{8{Pe&o}Ux=ITQ6d42;0l}OSEA&Oci$p?-BL187L6rJ>Q)aX0)Wf%T zneJF2;<-V%-VlcA?X03zpf;wI&8z9@Hy0BZm&ac-Gdtgo>}VkZYk##OOD+nVOKLFJ z5hgXAhkIzZtCU%2M#xl=D7EQPwh?^gZ_@0p$HLd*tF>qgA_P*dP;l^cWm&iQSPJZE zBoipodanrwD0}}{H#5o&PpQpCh61auqlckZq2_Eg__8;G-CwyH#h1r0iyD#Hd_$WgM89n+ldz;=b!@pvr4;x zs|YH}rQuCyZO!FWMy%lUyDE*0)(HR}QEYxIXFexCkq7SHmSUQ)2tZM2s`G<9dq;Vc ziNVj5hiDyqET?chgEA*YBzfzYh_RX#0MeD@xco%)ON%6B7E3#3iFBkPK^P_=&8$pf zpM<0>QmE~1FX1>mztm>JkRoosOq8cdJ1gF5?%*zMDak%qubN}SM!dW6fgH<*F>4M7 zX}%^g{>ng^2_xRNGi^a(epr8SPSP>@rg7s=0PO-#5*s}VOH~4GpK9<4;g=+zuJY!& ze_ld=ybcca?dUI-qyq2Mwl~-N%iCGL;LrE<#N}DRbGow7@5wMf&d`kT-m-@geUI&U z0NckZmgse~(#gx;tsChgNd|i1Cz$quL>qLzEO}ndg&Pg4f zy`?VSk9X5&Ab_TyKe=oiIiuNTWCsk6s9Ie2UYyg1y|i}B7h0k2X#YY0CZ;B7!dDg7 z_a#pK*I7#9-$#Iev5BpN@xMq@mx@TH@SoNWc5dv%^8!V}nADI&0K#xu_#y)k%P2m~ zqNqQ{(fj6X8JqMe5%;>MIkUDd#n@J9Dm~7_wC^z-Tcqqnsfz54jPJ1*+^;SjJzJhG zIq!F`Io}+fRD>h#wjL;g+w?Wg`%BZ{f()%Zj)sG8permeL0eQ9vzqcRLyZ?IplqMg zpQaxM11^`|6%3hUE9AiM5V)zWpPJ7nt*^FDga?ZP!U1v1aeYrV2Br|l`J^tgLm;~%gX^2l-L9L`B?UDHE9_+jaMxy|dzBY4 zjsR2rcZ6HbuyyXsDV(K0#%uPd#<^V%@9c7{6Qd_kQEZL&;z_Jf+eabr)NF%@Ulz_a1e(qWqJC$tTC! zwF&P-+~VN1Vt9OPf`H2N{6L@UF@=g+xCC_^^DZ`8jURfhR_yFD7#VFmklCR*&qk;A zzyw8IH~jFm+zGWHM5|EyBI>n3?2vq3W?aKt8bC+K1`YjklQx4*>$GezfU%E|>Or9Y zNRJ@s(>L{WBXdNiJiL|^In*1VA`xiE#D)%V+C;KuoQi{1t3~4*8 z;tbUGJ2@2@$XB?1!U;)MxQ}r67D&C49k{ceku^9NyFuSgc}DC2pD|+S=qLH&L}Vd4 zM=-UK4{?L?xzB@v;qCy}Ib65*jCWUh(FVc&rg|+KnopG`%cb>t;RNv=1%4= z#)@CB7i~$$JDM>q@4ll8{Ja5Rsq0 z$^|nRac)f7oZH^=-VdQldC~E_=5%JRZSm!z8TJocv`w<_e0>^teZ1en^x!yQse%Lf z;JA5?0vUIso|MS03y${dX19A&bU4wXS~*T7h+*4cgSIX11EB?XGiBS39hvWWuyP{!5AY^x5j{!c?z<}7f-kz27%b>llPq%Z7hq+CU|Ev2 z*jh(wt-^7oL`DQ~Zw+GMH}V*ndCc~ zr>WVQHJQ8ZqF^A7sH{N5~PbeDihT$;tUP`OwWn=j6@L+!=T|+ze%YQ zO+|c}I)o_F!T(^YLygYOTxz&PYDh9DDiv_|Ewm~i7|&Ck^$jsv_0n_}q-U5|_1>*L44)nt!W|;4q?n&k#;c4wpSx5atrznZbPc;uQI^I}4h5Fy`9J)l z7yYa7Rg~f@0oMHO;seQl|E@~fd|532lLG#e6n#vXrfdh~?NP){lZ z&3-33d;bUTEAG=!4_{YHd3%GCV=WS|2b)vZgX{JC)?rsljjzWw@Hflbwg3kIs^l%y zm3fVP-55Btz;<-p`X(ohmi@3qgdHmwXfu=gExL!S^ve^MsimP zNCBV>2>=BjLTobY^67f;8mXQ1YbM_NA3R^s z{zhY+5@9iYKMS-)S>zSCQuFl!Sd-f@v%;;*fW5hme#xAvh0QPtJ##}b>&tth$)6!$ z0S&b2OV-SE<|4Vh^8rs*jN;v9aC}S2EiPKo(G&<6C|%$JQ{;JEg-L|Yob*<-`z?AsI(~U(P>cC=1V$OETG$7i# zG#^QwW|HZuf3|X|&86lOm+M+BE>UJJSSAAijknNp*eyLUq=Au z7&aqR(x8h|>`&^n%p#TPcC@8@PG% zM&7k6IT*o-NK61P1XGeq0?{8kA`x;#O+|7`GTcbmyWgf^JvWU8Y?^7hpe^85_VuRq7yS~8uZ=Cf%W^OfwF_cbBhr`TMw^MH0<{3y zU=y;22&oVlrH55eGNvoklhfPM`bPX`|C_q#*etS^O@5PeLk(-DrK`l|P*@#T4(kRZ z`AY7^%&{!mqa5}q%<=x1e29}KZ63=O>89Q)yO4G@0USgbGhR#r~OvWI4+yu4*F8o`f?EG~x zBCEND=ImLu2b(FDF3sOk_|LPL!wrzx_G-?&^EUof1C~A{feam{2&eAf@2GWem7! z|LV-lff1Dk+mvTw@=*8~0@_Xu@?5u?-u*r8E7>_l1JRMpi{9sZqYG+#Ty4%Mo$`ds zsVROZH*QoCErDeU7&=&-ma>IUM|i_Egxp4M^|%^I7ecXzq@K8_oz!}cHK#>&+$E4rs2H8Fyc)@Bva?(KO%+oc!+3G0&Rv1cP)e9u_Y|dXr#!J;n%T4+9rTF>^m_4X3 z(g+$G6Zb@RW*J-IO;HtWHvopoVCr7zm4*h{rX!>cglE`j&;l_m(FTa?hUpgv%LNV9 zkSnUu1TXF3=tX)^}kDZk|AF%7FmLv6sh?XCORzhTU%d>y4cC;4W5mn=i6vLf2 ztbTQ8RM@1gn|y$*jZa8&u?yTOlNo{coXPgc%s;_Y!VJw2Z1bf%57p%kC1*5e{bepl zwm?2YGk~x=#69_Ul8A~(BB}>UP27=M)#aKrxWc-)rLL+97=>x|?}j)_5ewvoAY?P| z{ekQQbmjbGC%E$X*x-M=;Fx}oLHbzyu=Dw>&WtypMHnOc92LSDJ~PL7sU!}sZw`MY z&3jd_wS8>a!si2Y=ijCo(rMnAqq z-o2uzz}Fd5wD%MAMD*Y&=Ct?|B6!f0jfiJt;hvkIyO8me(u=fv_;C;O4X^vbO}R_% zo&Hx7C@EcZ!r%oy}|S-8CvPR?Ns0$j`FtMB;h z`#0Qq)+6Fxx;RCVnhwp`%>0H4hk(>Kd!(Y}>U+Tr_6Yp?W%jt_zdusOcA$pTA z(4l9$K=VXT2ITDs!OcShuUlG=R6#x@t74B2x7Dle%LGwsZrtiqtTuZGFUio_Xwpl} z=T7jdfT~ld#U${?)B67E*mP*E)XebDuMO(=3~Y=}Z}rm;*4f~7ka196QIHj;JK%DU z?AQw4I4ZufG}gmfVQ3w{snkpkgU~Xi;}V~S5j~;No^-9eZEYvA`Et=Q4(5@qcK=Pr zk9mo>v!%S>YD^GQc7t4c!C4*qU76b}r(hJhO*m-s9OcsktiXY#O1<OoH z#J^Y@1A;nRrrxNFh?3t@Hx9d>EZK*kMb-oe`2J!gZ;~I*QJ*f1p93>$lU|4qz!_zH z&mOaj#(^uiFf{*Nq?_4&9ZssrZeCgj1J$1VKn`j+bH%9#C5Q5Z@9LYX1mlm^+jkHf z+CgcdXlX5);Ztq6OT@;UK_zG(M5sv%I`d2(i1)>O`VD|d1_l(_aH(h>c7fP_$LA@d z6Wgm))NkU!v^YaRK_IjQy-_+>f_y(LeS@z+B$5be|FzXqqg}`{eYpO;sXLrU{*fJT zQHUEXoWk%wh%Kal`E~jiu@(Q@&d&dW*!~9;T=gA{{~NJwQvULf;s43Ku#A$NgaR^1 z%U3BNX`J^YE-#2dM*Ov*CzGdP9^`iI&`tmD~Bwqy4*N=DHt%RycykhF* zc7BcXG28Jvv(5G8@-?OATk6|l{Rg1 zwdU2Md1Qv?#$EO3E}zk&9>x1sQiD*sO0dGSUPkCN-gjuppdE*%*d*9tEWyQ%hRp*7 zT`N^=$PSaWD>f;h@$d2Ca7 z8bNsm14sdOS%FQhMn9yC83$ z-YATg3X!>lWbLUU7iNk-`O%W8MrgI03%}@6l$9+}1KJ1cTCiT3>^e}-cTP&aEJcUt zCTh_xG@Oa-v#t_UDKKfd#w0tJfA+Ash!0>X&`&;2%qv$!Gogr4*rfMcKfFl%@{ztA zwoAarl`DEU&W_DUcIq-{xaeRu(ktyQ64-uw?1S*A>7pRHH5_F)_yC+2o@+&APivkn zwxDBp%e=?P?3&tiVQb8pODI}tSU8cke~T#JLAxhyrZ(yx)>fUhig`c`%;#7Ot9le# zSaep4L&sRBd-n&>6=$R4#mU8>T>=pB)feU9;*@j2kyFHIvG`>hWYJ_yqv?Kk2XTw` z42;hd=hm4Iu0h{^M>-&c9zKPtqD>+c$~>k&Wvq#>%FjOyifO%RoFgh*XW$%Hz$y2-W!@W6+rFJja=pw-u_s0O3WMVgLb&CrCQ)8I^6g!iQj%a%#h z<~<0S#^NV4n!@tiKb!OZbkiSPp~31?f9Aj#fosfd*v}j6&7YpRGgQ5hI_eA2m+Je) zT2QkD;A@crBzA>7T zw4o1MZ_d$)puHvFA2J|`IwSXKZyI_iK_}FvkLDaFj^&6}e|5@mrHr^prr{fPVuN1+ z4=9}DkfKLYqUq7Q7@qa$)o6&2)kJx-3|go}k9HCI6ahL?NPA&khLUL}k_;mU&7GcN zNG6(xXW}(+a%IT80=-13-Q~sBo>$F2m`)7~wjW&XKndrz8soC*br=F*A_>Sh_Y}2Mt!#A1~2l?|hj) z9wpN&jISjW)?nl{@t`yuLviwvj)vyZQ4KR#mU-LE)mQ$yThO1oohRv;93oEXE8mYE zXPQSVCK~Lp3hIA_46A{8DdA+rguh@98p?VG2+Nw(4mu=W(sK<#S`IoS9nwuOM}C0) zH9U|6N=BXf!jJ#o;z#6vi=Y3NU5XT>ZNGe^z4u$i&x4ty^Sl;t_#`|^hmur~;r;o- z*CqJb?KWBoT`4`St5}10d*RL?!hm`GaFyxLMJPgbBvjVD??f7GU9*o?4!>NabqqR! z{BGK7%_}96G95B299eErE5_rkGmSWKP~590$HXvsRGJN5-%6d@=~Rs_68BLA1RkZb zD%ccBqGF0oGuZ?jbulkt!M}{S1;9gwAVkgdilT^_AS`w6?UH5Jd=wTUA-d$_O0DuM z|9E9XZFl$tZctd`Bq=OfI(cw4A)|t zl$W~3_RkP zFA6wSu+^efs79KH@)0~c3Dn1nSkNj_s)qBUGs6q?G0vjT&C5Y3ax-seA_+_}m`aj} zvW04)0TSIpqQkD@#NXZBg9z@GK1^ru*aKLrc4{J0PjhNfJT}J;vEeJ1ov?*KVNBy< zXtNIY3TqLZ=o1Byc^wL!1L6#i6n(088T9W<_iu~$S&VWGfmD|wNj?Q?Dnc#6iskoG zt^u26JqFnt=xjS-=|ACC%(=YQh{_alLW1tk;+tz1ujzeQ--lEu)W^Jk>UmHK(H303f}P2i zrsrQ*nEz`&{V!%2O446^8qLR~-Pl;2Y==NYj^B*j1vD}R5plk>%)GZSSjbi|tx>YM zVd@IS7b>&Uy%v==*35wGwIK4^iV{31mc)dS^LnN8j%#M}s%B@$=bPFI_ifcyPd4hilEWm71chIwfIR(-SeQaf20{;EF*(K(Eo+hu{}I zZkjXyF}{(x@Ql~*yig5lAq7%>-O5E++KSzEe(sqiqf1>{Em)pN`wf~WW1PntPpzKX zn;14G3FK7IQf!~n>Y=cd?=jhAw1+bwlVcY_kVuRyf!rSFNmR4fOc(g7(fR{ANvcO< zbG|cnYvKLa>dU(Z9YP796`Au?gz)Ys?w!af`F}1#W>x_O|k9Q z>#<6bKDt3Y}?KT2tmhU>H6Umn}J5M zarILVggiZs=kschc2TKib2`gl^9f|(37W93>80keUkrC3ok1q{;PO6HMbm{cZ^ROcT#tWWsQy?8qKWt<42BGryC(Dx>^ohIa0u7$^)V@Bn17^(VUgBD> zAr*Wl6UwQ&AAP%YZ;q2cZ;@2M(QeYFtW@PZ+mOO5gD1v-JzyE3^zceyE5H?WLW?$4 zhBP*+3i<09M$#XU;jwi7>}kW~v%9agMDM_V1$WlMV|U-Ldmr|<_nz*F_kcgrJnrViguEnJt{=Mk5f4Foin7(3vUXC>4gyJ>sK<;-p{h7 z2_mr&Fca!E^7R6VvodGznqJn3o)Ibd`gk>uKF7aemX*b~Sn#=NYl5j?v*T4FWZF2D zaX(M9hJ2YuEi%b~4?RkJwT*?aCRT@ecBkq$O!i}EJJEw`*++J_a>gsMo0CG^pZ3x+ zdfTSbCgRwtvAhL$p=iIf7%Vyb!j*UJsmOMler--IauWQ;(ddOk+U$WgN-RBle~v9v z9m2~@h|x*3t@m+4{U2}fKzRoVePrF-}U{`YT|vW?~64Bv*7|Dz03 zRYM^Yquhf*ZqkN?+NK4Ffm1;6BR0ZyW3MOFuV1ljP~V(=-tr^Tgu#7$`}nSd<8?cP z`VKtIz5$~InI0YnxAmn|pJZj+nPlI3zWsykXTKRnDCBm~Dy*m^^qTuY+8dSl@>&B8~0H$Y0Zc25APo|?R= z>_#h^kcfs#ae|iNe{BWA7K1mLuM%K!_V?fDyEqLkkT&<`SkEJ;E+Py^%hPVZ(%a2P4vL=vglF|X_`Z$^}q470V+7I4;UYdcZ7vU=41dd{d#KmI+|ZGa>C10g6w1a?wxAc&?iYsEv zuCwWvcw4FoG=Xrq=JNyPG*yIT@xbOeV`$s_kx`pH0DXPf0S7L?F208x4ET~j;yQ2c zhtq=S{T%82U7GxlUUKMf-NiuhHD$5*x{6}}_eZ8_kh}(}BxSPS9<(x2m$Rn0sx>)a zt$+qLRJU}0)5X>PXVxE?Jxpw(kD0W43ctKkj8DjpYq}lFZE98Je+v2t7uxuKV;p0l z5b9smYi5~k2%4aZe+~6HyobTQ@4_z#*lRHl# zSA`s~Jl@RGq=B3SNQF$+puBQv>DaQ--V!alvRSI~ZoOJx3VP4sbk!NdgMNBVbG&BX zdG*@)^g4#M#qoT`^NTR538vx~rdyOZcfzd7GBHl68-rG|fkofiGAXTJx~`~%a&boY zZ#M4sYwHIOnu-Mr!Ltpl8!NrX^p74tq{f_F4%M@&<=le;>xc5pAi&qn4P>04D$fp` z(OuJXQia--?vD0DIE6?HC|+DjH-?Cl|GqRKvs8PSe027_NH=}+8km9Ur8(JrVx@*x z0lHuHd=7*O+&AU_B;k{>hRvV}^Uxl^L1-c-2j4V^TG?2v66BRxd~&-GMfcvKhWgwu z60u{2)M{ZS)r*=&J4%z*rtqs2syPiOQq(`V0UZF)boPOql@E0U39>d>MP=BqFeJzz zh?HDKtY3%mR~reR7S2rsR0aDMA^a|L^_*8XM9KjabpYSBu z;zkfzU~12|X_W_*VNA=e^%Za14PMOC!z`5Xt|Fl$2bP9fz>(|&VJFZ9{z;;eEGhOl zl7OqqDJzvgZvaWc7Nr!5lfl*Qy7_-fy9%f(v#t#&2#9o-ba%J3(%s#C=@dagx*I{d zB&AzGT9EEiknWJU^naNdz7Logo%#OFV!eyCIQuzgpZDDN-1F}JJTdGXiLN85p|GT! zGOfNd8^RD;MsK*^3gatg2#W0J<8j)UCkUYoZRR|R*UibOm-G)S#|(`$hPA7UmH+fT ziZxTgeiR_yzvNS1s+T!xw)QgNSH(_?B@O?uTBwMj`G)2c^8%g8zu zxMu5SrQ^J+K91tkPrP%*nTpyZor#4`)}(T-Y8eLd(|sv8xcIoHnicKyAlQfm1YPyI z!$zimjMlEcmJu?M6z|RtdouAN1U5lKmEWY3gajkPuUHYRvTVeM05CE@`@VZ%dNoZN z>=Y3~f$~Gosud$AN{}!DwV<6CHm3TPU^qcR!_0$cY#S5a+GJU-2I2Dv;ktonSLRRH zALlc(lvX9rm-b5`09uNu904c}sU(hlJZMp@%nvkcgwkT;Kd7-=Z_z9rYH@8V6Assf zKpXju&hT<=x4+tCZ{elYtH+_F$V=tq@-`oC%vdO>0Wmu#w*&?_=LEWRJpW|spYc8V z=$)u#r}Pu7kvjSuM{FSyy9_&851CO^B zTm$`pF+lBWU!q>X#;AO1&=tOt=i!=9BVPC#kPJU}K$pO&8Ads)XOFr336_Iyn z$d{MTGYQLX9;@mdO;_%2Ayw3hv}_$UT00*e{hWxS?r=KT^ymEwBo429b5i}LFmSk` zo)-*bF1g;y@&o=34TW|6jCjUx{55EH&DZ?7wB_EmUg*B4zc6l7x-}qYLQR@^7o6rrgkoujRNym9O)K>wNfvY+uy+4Om{XgRHi#Hpg*bZ36_X%pP`m7FIF z?n?G*g&>kt$>J_PiXIDzgw3IupL3QZbysSzP&}?JQ-6TN-aEYbA$X>=(Zm}0{hm6J zJnqQnEFCZGmT06LAdJ^T#o`&)CA*eIYu?zzDJi#c$1H9zX}hdATSA|zX0Vb^q$mgg z&6kAJ=~gIARct>}4z&kzWWvaD9#1WK=P>A_aQxe#+4cpJtcRvd)TCu! z>eqrt)r(`qYw6JPKRXSU#;zYNB7a@MYoGuAT0Nzxr`>$=vk`uEq2t@k9?jYqg)MXl z67MA3^5_}Ig*mycsGeH0_VtK3bNo;8#0fFQ&qDAj=;lMU9%G)&HL>NO|lWU3z+m4t7 zfV*3gSuZ++rIWsinX@QaT>dsbD>Xp8%8c`HLamm~(i{7L&S0uZ;`W-tqU4XAgQclM$PxE76OH(PSjHjR$(nh({vsNnawhP!!HcP!l)5 zG;C=k0xL<^q+4rpbp{sGzcc~ZfGv9J*k~PPl}e~t$>WPSxzi0}05(D6d<=5+E}Y4e z@_QZtDcC7qh4#dQFYb6Pulf_8iAYYE z1SWJfNe5@auBbE5O=oeO@o*H5mS(pm%$!5yz-71~lEN5=x0eN|V`xAeP;eTje?eC= z53WneK;6n35{OaIH2Oh6Hx)kV-jL-wMzFlynGI8Wk_A<~_|06rKB#Pi_QY2XtIGW_ zYr)RECK_JRzR1tMd(pM(L=F98y~7wd4QBKAmFF(AF(e~+80$GLZpFc;a{kj1h}g4l z3SxIRlV=h%Pl1yRacl^g>9q%>U+`P(J`oh-w8i82mFCn|NJ5oX*^VKODX2>~HLUky z3D(ak0Sj=Kv^&8dUhU(3Ab!U5TIy97PKQ))&`Ml~hik%cHNspUpCn24cqH@dq6ZVo zO9xz!cEMm;NL;#z-tThlFF%=^ukE8S0;hDMR_`rv#eTYg7io1w9n_vJpK+6%=c#Y?wjAs_(#RQA0gr&Va2BQTq` zUc8)wHEDl&Uyo<>-PHksM;b-y(`E_t8Rez@Iw+eogcEI*FDg@Bc;;?3j3&kPsq(mx z+Yr_J#?G6D?t2G%O9o&e7Gbf&>#(-)|8)GIbG_a${TU26cVrIQSt=% zQ~XY-b1VQVc>IV=7um0^Li>dF z`zSm_o*i@ra4B+Tw5jdguVqx`O(f4?_USIMJzLvS$*kvBfEuToq-VR%K*%1VHu=++ zQ`=cG3cCnEv{ZbP-h9qbkF}%qT$j|Z7ZB2?s7nK@gM{bAD=eoDKCCMlm4LG~yre!- zzPP#Rn9ZDUgb4++M78-V&VX<1ah(DN z(4O5b`Fif%*k?L|t%!WY`W$C_C`tzC`tI7XC`->oJs_Ezs=K*O_{*#SgNcvYdmBbG zHd8!UTzGApZC}n7LUp1fe0L<3|B5GdLbxX@{ETeUB2vymJgWP0q2E<&!Dtg4>v`aa zw(QcLoA&eK{6?Rb&6P0kY+YszBLXK49i~F!jr)7|xcnA*mOe1aZgkdmt4{Nq2!!SL z`aD{6M>c00muqJt4$P+RAj*cV^vn99UtJ*s${&agQ;C>;SEM|l%KoH_^kAcmX=%)* zHpByMU_F12iGE#68rHGAHO_ReJ#<2ijo|T7`{PSG)V-bKw}mpTJwtCl%cq2zxB__m zM_p2k8pDmwA*$v@cmm>I)TW|7a7ng*X7afyR1dcuVGl|BQzy$MM+zD{d~n#)9?1qW zdk(th4Ljb-vpv5VUt&9iuQBnQ$JicZ)+HoL`&)B^Jr9F1wvf=*1and~v}3u{+7u7F zf0U`l4Qx-ANfaB3bD1uIeT^zeXerps8nIW(tmIxYSL;5~!&&ZOLVug2j4t7G=zzK+ zmPy5<4h%vq$Fw)i1)ya{D;GyEm3fybsc8$=$`y^bRdmO{XU#95EZ$I$bBg)FW#=}s z@@&c?xwLF3|C7$%>}T7xl0toBc6N^C{!>a8vWc=G!bAFKmn{AKS6RxOWIJBZXP&0CyXAiHd?7R#S46K6UXYXl#c_#APL5SfW<<-|rcfX&B6e*isa|L^RK=0}D`4q-T0VAs0 zToyrF6`_k$UFGAGhY^&gg)(Fq0p%J{h?E)WQ(h@Gy=f6oxUSAuT4ir}jI)36|NnmnI|vtij;t!jT?6Jf-E19}9Lf9(+N+ z)+0)I5mST_?3diP*n2=ZONTYdXkjKsZ%E$jjU@0w_lL+UHJOz|K{{Uh%Zy0dhiqyh zofWXzgRyFzY>zpMC8-L^43>u#+-zlaTMOS(uS!p{Jw#u3_9s)(s)L6j-+`M5sq?f+ zIIcjq$}~j9b`0_hIz~?4?b(Sqdpi(;1=8~wkIABU+APWQdf5v@g=1c{c{d*J(X5+cfEdG?qxq z{GKkF;)8^H&Xdi~fb~hwtJRsfg#tdExEuDRY^x9l6=E+|fxczIW4Z29NS~-oLa$Iq z93;5$(M0N8ba%8&q>vFc=1}a8T?P~_nrL5tYe~X>G=3QoFlBae8vVt-K!^@vusN<8gQJ!WD7H%{*YgY0#(tXxXy##C@o^U7ysxe zLmUWN@4)JBjjZ3G-_)mrA`|NPCc8Oe!%Ios4$HWpBmJse7q?)@Xk%$x&lIY>vX$7L zpfNWlXxy2p7TqW`Wq22}Q3OC2OWTP_X(*#kRx1WPe%}$C!Qn^FvdYmvqgk>^nyk;6 zXv*S#P~NVx1n6pdbXuX9x_}h1SY#3ZyvLZ&VnWVva4)9D|i7kjGY{>am&^ z-_x1UYM1RU#z17=AruK~{BK$A65Sajj_OW|cpYQBGWO*xfGJXSn4E&VMWchq%>0yP z{M2q=zx!VnO71gb8}Al2i+uxb=ffIyx@oso@8Jb88ld6M#wgXd=WcX$q$91o(94Ek zjeBqQ+CZ64hI>sZ@#tjdL}JeJu?GS7N^s$WCIzO`cvj60*d&#&-BQ>+qK#7l+!u1t zBuyL-Cqups?2>)ek2Z|QnAqs_`u1#y8=~Hvsn^2Jtx-O`limc*w;byk^2D-!*zqRi zVcX+4lzwcCgb+(lROWJ~qi;q2!t6;?%qjGcIza=C6{T7q6_?A@qrK#+)+?drrs3U}4Fov+Y}`>M z#40OUPpwpaC-8&q8yW0XWGw`RcSpBX+7hZ@xarfCNnrl-{k@`@Vv> zYWB*T=4hLJ1SObSF_)2AaX*g(#(88~bVG9w)ZE91eIQWflNecYC zzUt}ov<&)S&i$}?LlbIi9i&-g=UUgjWTq*v$!0$;8u&hwL*S^V!GPSpM3PR3Ra5*d z7d77UC4M{#587NcZS4+JN=m#i)7T0`jWQ{HK3rIIlr3cDFt4odV25yu9H1!}BVW-& zrqM5DjDzbd^pE^Q<-$1^_tX)dX8;97ILK{ z!{kF{!h`(`6__+1UD5=8sS&#!R>*KqN9_?(Z$4cY#B)pG8>2pZqI;RiYW6aUt7kk*s^D~Rml_fg$m+4+O5?J&p1)wE zp5L-X(6og1s(?d7X#l-RWO+5Jj(pAS{nz1abM^O;8hb^X4pC7ADpzUlS{F~RUoZp^ zuJCU_fq}V!9;knx^uYD2S9E`RnEsyF^ZO$;`8uWNI%hZzKq=t`q12cKEvQjJ9dww9 zCerpM3n@Ag+XZJztlqHRs!9X(Dv&P;_}zz$N&xwA@~Kfnd3}YiABK*T)Ar2E?OG6V z<;mFs`D?U7>Rradv7(?3oCZZS_0Xr#3NNkpM1@qn-X$;aNLYL;yIMX4uubh^Xb?HloImt$=^s8vm)3g!{H1D|k zmbg_Rr-ypQokGREIcG<8u(=W^+oxelI&t0U`dT=bBMe1fl+9!l&vEPFFu~yAu!XIv4@S{;| z8?%<1@hJp%7AfZPYRARF1hf`cq_VFQ-y74;EdMob{z&qec2hiQJOQa>f-?Iz^VXOr z-wnfu*uT$(5WmLsGsVkHULPBvTRy0H(}S0SQ18W0kp_U}8Phc3gz!Hj#*VYh$AiDE245!YA0M$Q@rM zT;}1DQ}MxV<)*j{hknSHyihgMPCK=H)b-iz9N~KT%<&Qmjf39L@&7b;;>9nQkDax- zk%7ZMA%o41l#(G5K=k{D{80E@P|I;aufYpOlIJXv!dS+T^plIVpPeZ)Gp`vo+?BWt z8U8u=C51u%>yDCWt>`VGkE5~2dD4y_8+n_+I9mFN(4jHJ&x!+l*>%}b4Z>z#(tb~< z+<+X~GIi`sDb=SI-7m>*krlqE3aQD?D5WiYX;#8m|ENYKw}H^95u!=n=xr3jxhCB&InJ7>zgLJg;i?Sjjd`YW!2; z%+y=LwB+MMnSGF@iu#I%!mvt)aXzQ*NW$cHNHwjoaLtqKCHqB}LW^ozBX?`D4&h%# zeMZ3ZumBn}5y9&odo3=hN$Q&SRte*^-SNZg2<}6>OzRpF91oy0{RuZU(Q0I zvx%|9>;)-Ca9#L)HQt~axu0q{745Ac;s1XQKV ze3D9I5gV5SP-J>&3U!lg1`HN>n5B6XxYpwhL^t0Z)4$`YK93vTd^7BD%<)cIm|4e!;*%9}B-3NX+J*Nr@;5(27Zmf(TmfHsej^Bz+J1 zXKIjJ)H{thL4WOuro|6&aPw=-JW8G=2 z|L4YL)^rYf7J7DOKXpTX$4$Y{-2B!jT4y^w8yh3LKRKO3-4DOshFk}N^^Q{r(0K0+ z?7w}x>(s{Diq6K)8sy)>%*g&{u>)l+-Lg~=gteW?pE`B@FE`N!F-+aE;XhjF+2|RV z8vV2((yeA-VDO;3=^E;fhW~b=Wd5r8otQrO{Vu)M1{j(+?+^q%xpYCojc6rmQ<&ytZ2ly?bw*X)WB8(n^B4Gmxr^1bQ&=m;I4O$g{ z3m|M{tmkOyAPnMHu(Z}Q1X1GM|A+)VDP3Fz934zSl)z>N|D^`G-+>Mej|VcK+?iew zQ3=DH4zz;i>z{Yv_l@j*?{936kxM{c7eK$1cf8wxL>>O#`+vsu*KR)te$adfTD*w( zAStXnZk<6N3V-Vs#GB%vXZat+(EFWbkbky#{yGY`rOvN)?{5qUuFv=r=dyYZrULf%MppWuNRUWc z8|YaIn}P0DGkwSZ(njAO$Zhr3Yw`3O1A+&F*2UjO{0`P%kK(qL;kEkfjRC=lxPRjL z{{4PO3-*5RZ_B3LUB&?ZpJ4nk1E4L&eT~HX0Jo(|uGQCW3utB@p)rF@W*n$==TlS zKiTfzhrLbAeRqru%D;fUwXOUcHud{pw@Ib1xxQ}<2)?KC&%y5PVef<7rcu2l!8dsy z?lvdaHJ#s$0m18y{x#fB$o=l)-sV?Qya5GWf#8Vd{~Grn@qgX#!EI`Y>++l%1A;eL z{_7t6jMeEr@a+oxyCL^+_}9Qc;i0&Xd%LXp?to*R|26LKHG(m0)*QF4*h;5%YG5<9)c> z1vq!7bIJSv1^27i-mcH!zX>ep3Iw0^{nx<1jOy)N_UoFD8v}x~2mEWapI3m~kMQkR z#&@4FuEGBn`mgtSx6jeY7vUQNf=^}sTZErIEpH!cy|@7Z zU4h_Oxxd2s=f{}$XXy4}%JqTSjRC - 3.3.0 - 3.3.0 + 3.3.2-SNAPSHOT + 3.3.1 3.0.0-SNAPSHOT spring.data.couchbase 2.10.13 @@ -233,6 +233,18 @@ 4.0.3 test + + org.testcontainers + testcontainers + + + + ch.qos.logback + logback-classic + 1.2.5 + compile + + @@ -251,10 +263,6 @@ false - - jitpack.io - https://jitpack.io - @@ -286,6 +294,7 @@ org.apache.maven.plugins maven-failsafe-plugin + false **/*IntegrationTest.java **/*IntegrationTests.java diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java new file mode 100644 index 000000000..9bbdc44ac --- /dev/null +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -0,0 +1,36 @@ +/* +/* + * Copyright 2021 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 com.couchbase.client.java.transactions; + +import java.lang.reflect.Field; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.codec.JsonSerializer; + +/** + * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext + * + * @author Michael Reiche + */ +@Stability.Internal +public class AttemptContextReactiveAccessor { + public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext( + CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { + return new ReactiveTransactionAttemptContext(core, jsonSerializer); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 51781a7d8..875c48b92 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -18,6 +18,9 @@ import java.io.Closeable; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; @@ -72,5 +75,4 @@ public interface CouchbaseClientFactory extends Closeable { * The exception translator used on the factory. */ PersistenceExceptionTranslator getExceptionTranslator(); - } diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index b97b57f95..06b16e017 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -15,8 +15,12 @@ */ package org.springframework.data.couchbase; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.function.Supplier; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; @@ -28,7 +32,11 @@ import com.couchbase.client.java.ClusterOptions; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; +import com.couchbase.client.java.codec.JsonSerializer; import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -44,18 +52,18 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { private final PersistenceExceptionTranslator exceptionTranslator; public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName) { + final String bucketName) { this(connectionString, authenticator, bucketName, null); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName) { + final String bucketName, final String scopeName) { this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator))), bucketName, scopeName); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName, final ClusterEnvironment environment) { + final String bucketName, final String scopeName, final ClusterEnvironment environment) { this( new OwnedSupplier<>( Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator).environment(environment))), @@ -67,7 +75,7 @@ public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketNa } private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, - final String scopeName) { + final String scopeName) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); @@ -97,9 +105,9 @@ public Scope getScope() { @Override public Collection getCollection(final String collectionName) { final Scope scope = getScope(); - if (collectionName == null) { + if (collectionName == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionName)) { if (!scope.name().equals(CollectionIdentifier.DEFAULT_SCOPE)) { - throw new IllegalStateException("A collectionName must be provided if a non-default scope is used!"); + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used"); } return getBucket().defaultCollection(); } @@ -123,4 +131,8 @@ public void close() { } } + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + } 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 6982d64f4..52e974201 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -17,15 +17,18 @@ package org.springframework.data.couchbase.config; import static com.couchbase.client.java.ClusterOptions.clusterOptions; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_MAPPING_CONTEXT; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -40,9 +43,16 @@ import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionInterceptor; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -57,7 +67,6 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JacksonTransformers; import com.couchbase.client.java.json.JsonValueModule; -import com.couchbase.client.java.query.QueryScanConsistency; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -123,15 +132,21 @@ protected Authenticator authenticator() { * @param couchbaseCluster the cluster reference from the SDK. * @return the initialized factory. */ - @Bean + @Bean(name = BeanNames.COUCHBASE_CLIENT_FACTORY) public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) { return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); } - +/* + @Bean + public ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory(final Cluster couchbaseCluster) { + return new SimpleReactiveCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); + } +*/ @Bean(destroyMethod = "disconnect") public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment) { - return Cluster.connect(getConnectionString(), + Cluster c = Cluster.connect(getConnectionString(), clusterOptions(authenticator()).environment(couchbaseClusterEnvironment)); + return c; } @Bean(destroyMethod = "shutdown") @@ -156,24 +171,29 @@ 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, - getDefaultConsistency()); + MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { + return new CouchbaseTemplate(couchbaseClientFactory, + mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return couchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); + MappingCouchbaseConverter mappingCouchbaseConverter) { + return couchbaseTemplate(couchbaseClientFactory, + mappingCouchbaseConverter, + new JacksonTranslationService()); } @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, - getDefaultConsistency()); + return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { return reactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); @@ -280,9 +300,8 @@ public TranslationService couchbaseTranslationService() { /** * Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package. - * */ - @Bean + @Bean(COUCHBASE_MAPPING_CONTEXT) public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customConversions) throws Exception { CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -310,6 +329,43 @@ public ObjectMapper couchbaseObjectMapper() { return mapper; } + /** + * The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager + * CallbackPreferrringTransactionmanagers do not play well with test-cases that rely + * on @TestTransaction/@BeforeTransaction/@AfterTransaction + * + * @param clientFactory + * @return + */ + @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + CouchbaseCallbackTransactionManager callbackTransactionManager(CouchbaseClientFactory clientFactory) { + return new CouchbaseCallbackTransactionManager(clientFactory); + } + + /** + * The default TransactionalOperator. + * + * @param couchbaseCallbackTransactionManager + * @return + */ + @Bean(BeanNames.COUCHBASE_TRANSACTIONAL_OPERATOR) + public CouchbaseTransactionalOperator transactionalOperator( + CouchbaseCallbackTransactionManager couchbaseCallbackTransactionManager) { + return CouchbaseTransactionalOperator.create(couchbaseCallbackTransactionManager); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionInterceptor transactionInterceptor(TransactionManager couchbaseTransactionManager) { + TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(couchbaseTransactionManager, transactionAttributeSource); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + if (couchbaseTransactionManager != null) { + interceptor.setTransactionManager(couchbaseTransactionManager); + } + return interceptor; + } + /** * Configure whether to automatically create indices for domain types by deriving the from the entity or not. */ @@ -375,5 +431,4 @@ private boolean nonShadowedJacksonPresent() { public QueryScanConsistency getDefaultConsistency() { return null; } - } diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index cb9bf63ea..014244fee 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -59,4 +59,10 @@ public class BeanNames { * The name for the bean that will handle reactive audit trail marking of entities. */ public static final String REACTIVE_COUCHBASE_AUDITING_HANDLER = "reactiveCouchbaseAuditingHandler"; + + public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory"; + + public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager"; + + public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator" ; } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java new file mode 100644 index 000000000..5a9e3aae1 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -0,0 +1,208 @@ +/* + * Copyright 2021 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; + +import org.springframework.util.ClassUtils; + +import java.util.Map; +import java.util.Set; + +public abstract class AbstractTemplateSupport { + + final ReactiveCouchbaseTemplate template; + final CouchbaseConverter converter; + final MappingContext, CouchbasePersistentProperty> mappingContext; + final TranslationService translationService; + ApplicationContext applicationContext; + static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class); + + public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter, TranslationService translationService) { + this.template = template; + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + this.translationService = translationService; + } + + abstract ReactiveCouchbaseTemplate getReactiveTemplate(); + + public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + final CouchbaseDocument converted = new CouchbaseDocument(id); + converted.setId(id); + + // this is the entity class defined for the repository. It may not be the class of the document that was read + // we will reset it after reading the document + // + // This will fail for the case where: + // 1) The version is defined in the concrete class, but not in the abstract class; and + // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in + // the source is null. + // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, + // but that is a lot of work to do every time just for this very rare and avoidable case. + // TypeInformation typeToUse = typeMapper.readType(source, type); + + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); + + if (persistentEntity == null) { // method could return a Long, Boolean, String etc. + // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left + // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a + // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. + // if this is a Collection or array, only the first element will be returned. + Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) + .getContent().entrySet(); + return (T) set.iterator().next().getValue(); + } + + // if possible, set the version property in the source so that if the constructor has a long version argument, + // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure + // can be avoid by defining the argument as Long instead of long. + // persistentEntity is still the (possibly abstract) class specified in the repository definition + // it's possible that the abstract class does not have a version property, and this won't be able to set the version + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + converted.put(persistentEntity.getVersionProperty().getName(), cas); + } + + // if the constructor has an argument that is long version, then construction will fail if the 'version' + // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. + // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) + T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); + + persistentEntity = couldBePersistentEntity(readEntity.getClass()); + + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + accessor.setProperty(persistentEntity.getVersionProperty(), cas); + } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection); + + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + } + + return accessor.getBean(); + } + + CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { + if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { + return null; + } + return mappingContext.getPersistentEntity(entityClass); + } + + + + public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + + final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() + .getRequiredPersistentEntity(entity.getClass()); + + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + if (idProperty != null) { + accessor.setProperty(idProperty, id); + } + + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + if (versionProperty != null) { + accessor.setProperty(versionProperty, cas); + } + + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + } + maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); + return (T) accessor.getBean(); + + } + + public Long getCas(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + long cas = 0; + if (versionProperty != null) { + Object casObject = accessor.getProperty(versionProperty); + if (casObject instanceof Number) { + cas = ((Number) casObject).longValue(); + } + } + return cas; + } + + public Object getId(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + Object id = null; + if (idProperty != null) { + id = accessor.getProperty(idProperty); + } + return id; + } + + public String getJavaNameForEntity(final Class clazz) { + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); + MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); + return info.getJavaType().getName(); + } + + ConvertingPropertyAccessor getPropertyAccessor(final T source) { + CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + } + + public void maybeEmitEvent(CouchbaseMappingEvent event) { + if (canPublishEvent()) { + try { + this.applicationContext.publishEvent(event); + } catch (Exception e) { + LOG.warn("{} thrown during {}", e, event); + throw e; + } + } else { + LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); + } + + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } + + public TranslationService getTranslationService(){ + return translationService; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java index e4d22669e..b9d3a0564 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java @@ -19,6 +19,7 @@ import java.util.ConcurrentModificationException; import java.util.concurrent.TimeoutException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -31,6 +32,7 @@ import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.core.error.*; +import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; /** * Simple {@link PersistenceExceptionTranslator} for Couchbase. @@ -98,6 +100,13 @@ public final DataAccessException translateExceptionIfPossible(final RuntimeExcep return new DataRetrievalFailureException(ex.getMessage(), ex); } + if (ex instanceof TransactionOperationFailedException) { + // Replace the TransactionOperationFailedException, since we want the Spring operation to fail with a + // Spring error. Internal state has already been set in the AttemptContext so the retry, rollback etc. + // will get respected regardless of what gets propagated (or not) from the lambda. + return new UncategorizedTransactionDataAccessException((TransactionOperationFailedException) ex); + } + // Unable to translate exception, therefore just throw the original! throw ex; } 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 33ce0791a..8cec31313 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -18,6 +18,9 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; + +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; import com.couchbase.client.java.query.QueryScanConsistency; @@ -50,4 +53,8 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { * Returns the default consistency to use for queries */ QueryScanConsistency getConsistency(); + T save(T entity); + + Long count(Query query, Class domainType); + } 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 4bf781210..ce7f3b69e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.core; +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -28,6 +30,7 @@ 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.query.Query; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -49,26 +52,32 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final CouchbaseTemplateSupport templateSupport; private final MappingContext, CouchbasePersistentProperty> mappingContext; private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + private final QueryScanConsistency scanConsistency; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; - private QueryScanConsistency scanConsistency; - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { - this(clientFactory, converter, new JacksonTranslationService()); + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final CouchbaseConverter converter) { + this(clientFactory, + converter, new JacksonTranslationService()); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + CouchbaseConverter converter, + final TranslationService translationService) { + this(clientFactory, + converter, translationService, null); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); - this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService, - scanConsistency); + this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, + translationService, scanConsistency); this.scanConsistency = scanConsistency; + this.mappingContext = this.converter.getMappingContext(); if (mappingContext instanceof CouchbaseMappingContext) { CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext; @@ -78,6 +87,20 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final Couch } } + public T save(T entity) { + if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { + return replaceById((Class) entity.getClass()).one(entity); + //} else if (getTransactionalOperator() != null) { + // return insertById((Class) entity.getClass()).one(entity); + } else { + return upsertById((Class) entity.getClass()).one(entity); + } + } + + public Long count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).count(); + } + @Override public ExecutableUpsertById upsertById(final Class domainType) { return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType); @@ -209,5 +232,4 @@ private void prepareIndexCreator(final ApplicationContext context) { public TemplateSupport support() { return templateSupport; } - } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 44a81d15b..f92fa6144 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -26,13 +26,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; @@ -41,10 +37,7 @@ import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import com.couchbase.client.core.error.CouchbaseException; @@ -57,23 +50,15 @@ * @author Carlos Espinaco * @since 3.0 */ -class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class); +class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport { private final CouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private EntityCallbacks entityCallbacks; - private ApplicationContext applicationContext; public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { + super(template.reactive(), converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -170,65 +155,35 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { } @Override - public Object applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - - return returnValue; + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); } @Override - public Object applyUpdatedId(final Object entity, Object id) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder, CouchbaseResourceHolder holder) { + return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @Override - public long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas,txResultHolder, null); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return applyResultBase(entity, converted, id, cas, txResultHolder, holder); } - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + @Override + public Integer getTxResultHolder(T source) { + return null; } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; @@ -252,24 +207,6 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected T maybeCallBeforeConvert(T object, String collection) { if (entityCallbacks != null) { return entityCallbacks.callback(BeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index 2aed295fb..58e9146ea 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -37,7 +37,8 @@ * * @author Christoph Strobl * @since 2.0 - */public interface ExecutableFindByAnalyticsOperation { + */ +public interface ExecutableFindByAnalyticsOperation { /** * Queries the analytics service. @@ -114,65 +115,23 @@ default Optional first() { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * * @param options to use for execution */ @Override - FindByAnalyticsWithQuery withOptions(AnalyticsOptions options); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByAnalyticsInCollection inScope(String scope); + TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { /** * Allows to override the default scan consistency. @@ -180,7 +139,7 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { * @param scanConsistency the custom scan consistency to use for this analytics query. */ @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); } @@ -194,10 +153,22 @@ interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWit FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { + + /** + * Set the filter for the analytics query to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); + + } + /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -207,9 +178,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ExecutableFindByAnalytics extends FindByAnalyticsWithProjection {} + interface ExecutableFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index 146359b90..b8284fe2a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -85,7 +85,7 @@ public List all() { } @Override - public TerminatingFindByAnalytics matching(final AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(final AnalyticsQuery query) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @@ -104,7 +104,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection != null ? collection : this.collection, options); } @@ -123,7 +123,7 @@ public FindByAnalyticsWithConsistency withConsistency(final AnalyticsScanCons } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 9cb60eeb7..00e43c2be 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -18,14 +18,15 @@ import java.time.Duration; import java.util.Collection; -import org.springframework.data.couchbase.core.support.OneAndAllId; import org.springframework.data.couchbase.core.support.InCollection; +import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; -import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import org.springframework.data.couchbase.core.support.WithExpiry; /** * Get Operations @@ -82,19 +83,57 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + @Override + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Provide attempt context + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Finds the distinct values for a specified {@literal field} across a single collection + * + * @return new instance of {@link ExecutableFindById}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + @Override + FindByIdWithProjection transaction(); + } + + interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -112,31 +151,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - @Override - FindByIdInScope project(String... fields); - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type to use for the results */ - interface ExecutableFindById extends FindByIdWithExpiry {} + interface ExecutableFindById extends FindByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 75c7856e5..a5cbd3f13 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -52,7 +52,7 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry) { + GetOptions options, List fields, Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -61,7 +61,8 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { this.fields = fields; this.expiry = expiry; this.reactiveSupport = new ReactiveFindByIdSupport<>(template.reactive(), domainType, scope, collection, options, - fields, expiry, new NonReactiveSupportWrapper(template.support())); + fields, expiry, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -81,7 +82,7 @@ public TerminatingFindById withOptions(final GetOptions options) { } @Override - public FindByIdWithOptions inCollection(final String collection) { + public FindByIdTxOrNot inCollection(final String collection) { return new ExecutableFindByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, fields, expiry); } @@ -93,13 +94,18 @@ public FindByIdInCollection inScope(final String scope) { @Override public FindByIdInScope project(String... fields) { Assert.notEmpty(fields, "Fields must not be null."); - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), + expiry); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, - expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + } + + @Override + public FindByIdWithExpiry transaction() { + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index f147dda67..716476f0b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -29,6 +29,7 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; @@ -128,42 +129,12 @@ default Optional first() { } - /** - * Fluent methods to specify the query - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - - /** - * Set the filter for the query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - @Override - TerminatingFindByQuery matching(Query query); - - /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. - * - * @param criteria must not be {@literal null}. - * @return new instance of {@link ExecutableFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. - */ - @Override - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - - } - /** * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** * Fluent method to specify options to use for execution * @@ -174,66 +145,85 @@ interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOp } /** - * Fluent method to specify the collection. + * To be removed at the next major release. use WithConsistency instead * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { + /** - * With a different collection + * Allows to override the default scan consistency. * - * @param collection the collection to use. + * @param scanConsistency the custom scan consistency to use for this query. */ - @Override - FindByQueryWithOptions inCollection(String collection); + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify the scope. + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this query. */ @Override - FindByQueryInCollection inScope(String scope); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * To be removed at the next major release. use WithConsistency instead - * + * Fluent method to specify transaction + * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { /** - * Allows to override the default scan consistency. + * Finds the distinct values for a specified {@literal field} across a single collection * - * @param scanConsistency the custom scan consistency to use for this query. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + @Override + TerminatingFindByQuery transaction(); } + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} + /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent methods to specify the query * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the query to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ @Override - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + FindByQueryTxOrNot matching(Query query); + + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + @Override + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -241,7 +231,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -251,7 +241,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -287,7 +277,37 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByQueryInCollection inScope(String scope); } /** @@ -295,6 +315,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ExecutableFindByQuery extends FindByQueryWithDistinct {} + interface ExecutableFindByQuery extends FindByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 571d6e995..d5e0f8280 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -63,8 +63,8 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final String[] fields; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, - final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, - final QueryOptions options, final String[] distinctFields, final String[] fields) { + final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, + final QueryOptions options, final String[] distinctFields, final String[] fields) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -96,7 +96,7 @@ public List all() { } @Override - public TerminatingFindByQuery matching(final Query query) { + public FindByQueryTxOrNot matching(final Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { scanCons = query.getScanConsistency(); @@ -121,7 +121,7 @@ public FindByQueryConsistentWith withConsistency(final QueryScanConsistency s } @Override - public FindByQueryWithConsistency as(final Class returnType) { + public FindByQueryWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields); @@ -136,7 +136,7 @@ public FindByQueryWithProjection project(String[] fields) { } @Override - public FindByQueryWithProjection distinct(final String[] distinctFields) { + public FindByQueryWithProjecting distinct(final String[] distinctFields) { Assert.notNull(distinctFields, "distinctFields must not be null"); Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified"); // Coming from an annotation, this cannot be null. @@ -147,6 +147,12 @@ public FindByQueryWithProjection distinct(final String[] distinctFields) { collection, options, dFields, fields); } + @Override + public FindByQueryWithDistinct transaction() { + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields); + } + @Override public Stream stream() { return reactiveSupport.all().toStream(); @@ -180,7 +186,7 @@ public FindByQueryInCollection inScope(final String scope) { } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection != null ? collection : this.collection, options, distinctFields, fields); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 0465d5022..ef4bb2295 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; @@ -73,8 +74,7 @@ interface TerminatingInsertById extends OneAndAllEntity { * * @param the entity type to use. */ - interface InsertByIdWithOptions - extends TerminatingInsertById, WithInsertOptions { + interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertOptions { /** * Fluent method to specify options to use for execution. * @@ -84,19 +84,42 @@ interface InsertByIdWithOptions TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithExpiry transaction(); + } + + interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -114,27 +137,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ExecutableInsertById extends InsertByIdWithExpiry {} + interface ExecutableInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index f853522a7..5c86f7e1e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -57,8 +57,8 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -69,7 +69,8 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { this.durabilityLevel = durabilityLevel; this.expiry = expiry; this.reactiveSupport = new ReactiveInsertByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -96,7 +97,7 @@ public InsertByIdInCollection inScope(final String scope) { } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @@ -123,6 +124,12 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { durabilityLevel, expiry); } + @Override + public InsertByIdWithExpiry transaction() { + return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 02381669e..48fb47d7e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -21,8 +21,10 @@ import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithCas; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -61,6 +63,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override RemoveResult one(String id); + /** + * Remove one document based on the entity. Transactions need the entity for the cas. + * + * @param entity the document ID. + * @return result of the remove + */ + RemoveResult oneEntity(Object entity); + /** * Remove the documents in the collection. * @@ -70,6 +80,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override List all(Collection ids); + /** + * Remove documents based on the entities. Transactions need the entity for the cas. + * + * @param entities to remove. + * @return result of the remove + */ + List allEntities(Collection entities); + } /** @@ -85,17 +103,39 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + + @Override + RemoveByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { + @Override + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends TerminatingRemoveById, WithTransaction { + @Override + TerminatingRemoveById transaction(); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** @@ -111,24 +151,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ExecutableRemoveById extends RemoveByIdWithCas {} + interface ExecutableRemoveById extends RemoveByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 717d11a98..710d79410 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -63,8 +63,8 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -76,20 +76,31 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); this.cas = cas; - } + } @Override public RemoveResult one(final String id) { return reactiveRemoveByIdSupport.one(id).block(); } + @Override + public RemoveResult oneEntity(final Object entity) { + return reactiveRemoveByIdSupport.oneEntity(entity).block(); + } + @Override public List all(final Collection ids) { return reactiveRemoveByIdSupport.all(ids).collectList().block(); } @Override - public RemoveByIdWithOptions inCollection(final String collection) { + public List allEntities(final Collection entities) { + return reactiveRemoveByIdSupport.allEntities(entities).collectList().block(); + } + + + @Override + public RemoveByIdTxOrNot inCollection(final String collection) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, cas); } @@ -127,6 +138,13 @@ public RemoveByIdWithDurability withCas(Long cas) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } + + @Override + public RemoveByIdWithCas transaction() { + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index a6bfdf0cf..d385e1a58 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -21,9 +21,9 @@ import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; -import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -56,32 +56,58 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith/*, WithConsistency */{ + //@Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { - /** - * Fluent method to specify options to use for execution - * - * @param options to use for execution - */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { + @Override + TerminatingRemoveByQuery transaction(); + } + + interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryWithTxOrNot, WithQuery { + + RemoveByQueryWithTxOrNot matching(Query query); + + default RemoveByQueryWithTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -89,13 +115,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -112,25 +138,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ExecutableRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ExecutableRemoveByQuery extends RemoveByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 74e9e01d4..dfd4f6102 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -53,7 +53,7 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final QueryOptions options; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { this.template = template; this.domainType = domainType; this.query = query; @@ -71,7 +71,7 @@ public List all() { } @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryWithTxOrNot matching(final Query query) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @@ -107,6 +107,13 @@ public RemoveByQueryInCollection inScope(final String scope) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope != null ? scope : this.scope, collection, options); } + + @Override + public TerminatingRemoveByQuery transaction() { + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index 51ce8e98b..4dc60ed19 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -83,19 +84,41 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + @Override + ReplaceByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + ReplaceByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + @Override + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + // todo gp is this staying? It's confusing when doing ops.replaceById() inside @Transactional to get this transaction() method - unclear as a user whether I need to call it or not + @Override + TerminatingReplaceById transaction(); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -113,24 +136,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope { ReplaceByIdInCollection inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - @Override - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - @Override - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - @Override - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ExecutableReplaceById extends ReplaceByIdWithExpiry {} + interface ExecutableReplaceById extends ReplaceByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 26f3dcb19..6c868588f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -57,8 +57,8 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -69,7 +69,8 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById this.durabilityLevel = durabilityLevel; this.expiry = expiry; this.reactiveSupport = new ReactiveReplaceByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -83,7 +84,7 @@ public Collection all(Collection objects) { } @Override - public ReplaceByIdWithOptions inCollection(final String collection) { + public ReplaceByIdTxOrNot inCollection(final String collection) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @@ -110,6 +111,12 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { replicateTo, durabilityLevel, expiry); } + @Override + public ReplaceByIdWithExpiry transaction() { + return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, + replicateTo, durabilityLevel, expiry); + } + @Override public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index 0831f8eb4..56f93d02a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -83,19 +83,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -113,25 +126,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope { UpsertByIdInCollection inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ExecutableUpsertById extends UpsertByIdWithExpiry {} + interface ExecutableUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 7cb19f82d..f8769c743 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,10 +15,12 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. @@ -40,33 +42,47 @@ public Mono encodeEntity(Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyUpdatedCas(Object entity, CouchbaseDocument converted, long cas) { - return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, converted, cas)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override - public Mono applyUpdatedId(Object entity, Object id) { - return Mono.fromSupplier(() -> support.applyUpdatedId(entity, id)); + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); } + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, holder)); + } + + @Override public Long getCas(Object entity) { return support.getCas(entity); } + @Override + public Object getId(Object entity) { + return support.getId(entity); + } + @Override public String getJavaNameForEntity(Class clazz) { return support.getJavaNameForEntity(clazz); } @Override - public void maybeEmitEvent(CouchbaseMappingEvent event) { - support.maybeEmitEvent(event); + public TranslationService getTranslationService() { + return support.getTranslationService(); } } 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 81b8cfdef..f5b0478e7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -17,6 +17,8 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import reactor.core.publisher.Mono; import com.couchbase.client.java.query.QueryScanConsistency; @@ -49,8 +51,12 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper */ CouchbaseClientFactory getCouchbaseClientFactory(); - /** - * @return the default consistency to use for queries - */ - QueryScanConsistency getConsistency(); + Mono save(T entity); + + Mono count(Query query, Class personClass); + + /** + * @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 a120b5280..ba833ca4d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.core; +import reactor.core.publisher.Mono; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -25,7 +27,12 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import com.couchbase.client.java.Collection; import com.couchbase.client.java.query.QueryScanConsistency; @@ -45,19 +52,16 @@ 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()); - } + private final QueryScanConsistency scanConsistency; - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final CouchbaseConverter converter) { + this(clientFactory, converter, new JacksonTranslationService(), null); } - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final CouchbaseConverter converter, final TranslationService translationService, + final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -65,6 +69,36 @@ public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, fin this.scanConsistency = scanConsistency; } + public Mono save(T entity) { + Assert.notNull(entity, "Entity must not be null!"); + Mono result; + final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() + .getPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); + final boolean versionPresent = versionProperty != null; + final Long version = versionProperty == null || versionProperty.getField() == null ? null + : (Long) ReflectionUtils.getField(versionProperty.getField(), entity); + final boolean existingDocument = version != null && version > 0; + + Class clazz = entity.getClass(); + + if (!versionPresent) { // the entity doesn't have a version property + // No version field - no cas + result = (Mono) upsertById(clazz).one(entity); + } else if (existingDocument) { // there is a version property, and it is non-zero + // Updating existing document with cas + result = (Mono) replaceById(clazz).one(entity); + } else { // there is a version property, but it's zero or not set. + // Creating new document + result = (Mono) insertById(clazz).one(entity); + } + return result; + } + + public Mono count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).all().count(); + } + @Override public ReactiveFindById findById(Class domainType) { return new ReactiveFindByIdOperationSupport(this).findById(domainType); @@ -165,8 +199,9 @@ public ReactiveTemplateSupport support() { * * @param ex the exception to translate */ - protected RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { - RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { + RuntimeException resolved = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) + : null; return resolved == null ? ex : resolved; } @@ -198,4 +233,57 @@ public QueryScanConsistency getConsistency() { return scanConsistency; } + /** + * Value object chaining together a given source document with its mapped representation and the collection to persist + * it to. + * + * @param + * @author Christoph Strobl + * @since 2.2 + */ + /* + private static class PersistableEntityModel { + + private final T source; + private final @Nullable + Document target; + private final String collection; + + private PersistableEntityModel(T source, @Nullable Document target, String collection) { + + this.source = source; + this.target = target; + this.collection = collection; + } + + static PersistableEntityModel of(T source, String collection) { + return new PersistableEntityModel<>(source, null, collection); + } + + static PersistableEntityModel of(T source, Document target, String collection) { + return new PersistableEntityModel<>(source, target, collection); + } + + PersistableEntityModel mutate(T source) { + return new PersistableEntityModel(source, target, collection); + } + + PersistableEntityModel addTargetDocument(Document target) { + return new PersistableEntityModel(source, target, collection); + } + + T getSource() { + return source; + } + + @Nullable + Document getTarget() { + return target; + } + + String getCollection() { + return collection; + } + + */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 6cfefd64c..2e5141e84 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -26,29 +26,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import reactor.core.publisher.Mono; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. @@ -56,23 +50,16 @@ * @author Carlos Espinaco * @since 4.2 */ -class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, ReactiveTemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(ReactiveCouchbaseTemplateSupport.class); +class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport + implements ApplicationContextAware, ReactiveTemplateSupport { private final ReactiveCouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private ReactiveEntityCallbacks reactiveEntityCallbacks; - private ApplicationContext applicationContext; public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { + super(template, converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -87,147 +74,35 @@ public Mono encodeEntity(final Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> { - // this is the entity class defined for the repository. It may not be the class of the document that was read - // we will reset it after reading the document - // - // This will fail for the case where: - // 1) The version is defined in the concrete class, but not in the abstract class; and - // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in - // the source is null. - // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, - // but that is a lot of work to do every time just for this very rare and avoidable case. - // TypeInformation typeToUse = typeMapper.readType(source, type); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - final CouchbaseDocument converted = new CouchbaseDocument(id); - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - if (id == null) { - throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID); - } - - final CouchbaseDocument converted = new CouchbaseDocument(id); - - // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure - // can be avoid by defining the argument as Long instead of long. - // persistentEntity is still the (possibly abstract) class specified in the repository definition - // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (persistentEntity.getVersionProperty() != null) { - if (cas == null) { - throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS - + " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS); - } - if (cas != 0) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - } - - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - persistentEntity = couldBePersistentEntity(readEntity.getClass()); - - if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection); - return accessor.getBean(); - }); - } - - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - try { - return mappingContext.getPersistentEntity(entityClass); - } catch (InaccessibleObjectException t) { - - } - return null; + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template; } @Override - public Mono applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - return Mono.fromSupplier(() -> { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - return returnValue; - }); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyUpdatedId(final Object entity, Object id) { - return Mono.fromSupplier(() -> { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; - }); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } - @Override - public Long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas, txResultHolder, null); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); } - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); - } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -252,24 +127,6 @@ public void setReactiveEntityCallbacks(ReactiveEntityCallbacks reactiveEntityCal this.reactiveEntityCallbacks = reactiveEntityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but ReactiveCouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected Mono maybeCallBeforeConvert(T object, String collection) { if (reactiveEntityCallbacks != null) { return reactiveEntityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 8436417d0..b318fbf41 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -72,9 +72,12 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, + domainType); LOG.trace("existsById key={} {}", id, pArgs); - return Mono.just(id) + + return TransactionalSupport.verifyNotInTransaction("existsById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) @@ -104,7 +107,7 @@ public ExistsByIdWithOptions inCollection(final String collection) { } @Override - public TerminatingExistsById withOptions(final ExistsOptions options) { + public ExistsByIdInScope withOptions(final ExistsOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index 1d661b302..2d9f1251b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -88,24 +88,12 @@ interface TerminatingFindByAnalytics extends OneAndAllReactive { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * @@ -115,65 +103,47 @@ interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, Wit TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } + @Deprecated + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Override - FindByAnalyticsInCollection inScope(String scope); + @Deprecated + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); + } - @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { /** * Allows to override the default scan consistency. * * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + @Override + FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } - interface FindByAnalyticsWithConsistency extends FindByAnalyticsInScope, WithAnalyticsConsistency { + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the analytics query to be used. * - * @param scanConsistency the custom scan consistency to use for this analytics query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - @Override - FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); } /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -183,9 +153,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ReactiveFindByAnalytics extends FindByAnalyticsWithProjection, FindByAnalyticsConsistentWith {} + interface ReactiveFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index a1599ac6d..10f84863c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -71,7 +71,7 @@ static class ReactiveFindByAnalyticsSupport implements ReactiveFindByAnalytic } @Override - public TerminatingFindByAnalytics matching(AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(AnalyticsQuery query) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } @@ -90,7 +90,7 @@ public FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scan } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); @@ -110,8 +110,9 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().reactive() - .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { + return TransactionalSupport.verifyNotInTransaction("findByAnalytics") + .then(template.getCouchbaseClientFactory().getCluster().reactive() + .analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { @@ -132,7 +133,7 @@ public Flux all() { } row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); - return support.decodeEntity(id, row.toString(), cas, returnType, null, null); + return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null); }); }); } @@ -172,7 +173,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection != null ? collection : this.collection, options, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 5e9983b0f..16001f177 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -27,11 +27,12 @@ import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; /** - * Get Operations + * Get Operations - method/interface chaining is from the bottom up. * * @author Christoph Strobl * @since 2.0 @@ -67,7 +68,20 @@ interface TerminatingFindById extends OneAndAllIdReactive { * @return the list of found entities. */ Flux all(Collection ids); + } + /** + * Provide transaction + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Provide transaction + * + * @return + */ + TerminatingFindById transaction(); } /** @@ -85,19 +99,45 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Interface to that can produce either transactional or non-transactional operations. + * + * @param the entity type to use for the results. + */ + interface FindByIdTxOrNot extends FindByIdWithTransaction, FindByIdWithExpiry {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -115,32 +155,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - FindByIdInCollection project(String... fields); - - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * - * @param the entity type to use for the results + * @param the entity type. */ - interface ReactiveFindById extends FindByIdWithExpiry {} + interface ReactiveFindById extends FindByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index a8391df04..156182dbe 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -16,10 +16,13 @@ package org.springframework.data.couchbase.core; import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -66,7 +69,8 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, ReactiveTemplateSupport support) { + CommonOptions options, List fields, Duration expiry, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -77,6 +81,8 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { this.support = support; } + + @Override public Mono one(final String id) { @@ -84,29 +90,43 @@ public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType); LOG.trace("findById key={} {}", id, pArgs); - return Mono.just(id).flatMap(docId -> { - ReactiveCollection reactive = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive(); - if (pArgs.getOptions() instanceof GetAndTouchOptions) { - return reactive.getAndTouch(docId, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()); + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive(); + + Mono reactiveEntity = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + if (!ctxOpt.isPresent()) { + System.err.println("find no-tx"); + if (pArgs.getOptions() instanceof GetAndTouchOptions) { + return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } else { + return rc.get(id, (GetOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } + } else { + System.err.println("find tx"); + return ctxOpt.get().getCore().get(makeCollectionIdentifier(rc.async()), id) + .flatMap(result -> support.decodeEntity(id, new String(result.contentAsBytes(), StandardCharsets.UTF_8), + result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), + new TransactionResultHolder(result), null)); + } + }); + + return reactiveEntity.onErrorResume(throwable -> { + if (throwable instanceof DocumentNotFoundException) { + return Mono.empty(); + } + return Mono.error(throwable); + }).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { - return reactive.get(docId, (GetOptions) pArgs.getOptions()); + return throwable; } - }).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection())).onErrorResume(throwable -> { - if (throwable instanceof RuntimeException) { - if (throwable instanceof DocumentNotFoundException) { - return Mono.empty(); - } - } - return Mono.error(throwable); - }).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + }); + } @Override @@ -115,9 +135,10 @@ public Flux all(final Collection ids) { } @Override - public TerminatingFindById withOptions(final GetOptions options) { + public FindByIdInScope withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, + support); } @Override @@ -133,15 +154,23 @@ public FindByIdInCollection inScope(final String scope) { } @Override - public FindByIdInScope project(String... fields) { + public FindByIdWithOptions project(String... fields) { Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), - expiry, support); + expiry, + support); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, + support); + } + + @Override + public FindByIdWithProjection transaction() { + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, + support); } private CommonOptions initGetOptions() { @@ -176,6 +205,7 @@ private Duration expiryToUse() { } return expiryToUse; } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index 9a839ed7a..f55d65da5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -28,6 +28,7 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -92,94 +93,98 @@ interface TerminatingFindByQuery extends OneAndAllReactive { } /** - * Fluent methods to filter by query + * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** - * Set the filter {@link Query} to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. + * @param options options to use for execution */ - TerminatingFindByQuery matching(Query query); + TerminatingFindByQuery withOptions(QueryOptions options); + } + + /** + * To be removed at the next major release. use WithConsistency instead + * + * @param the entity type to use for the results. + */ + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * Allows to override the default scan consistency. * - * @param criteria must not be {@literal null}. - * @return new instance of {@link TerminatingFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify options. - * + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + /** - * @param options options to use for execution + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this query. */ - TerminatingFindByQuery withOptions(QueryOptions options); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + } /** - * Fluent method to specify the collection - * + * Fluent method to add transactions + * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { - FindByQueryWithOptions inCollection(String collection); + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { + + /** + * Finds the distinct values for a specified {@literal field} across a single {@link } or view. + * + * @return new instance of {@link ReactiveFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + TerminatingFindByQuery transaction(); } /** - * Fluent method to specify the scope + * Fluent interface for operations with or without a transaction. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { - FindByQueryInCollection inScope(String scope); - } + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} /** - * To be removed at the next major release. use WithConsistency instead + * Fluent methods to filter by query * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter {@link Query} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + FindByQueryTxOrNot matching(Query query); /** - * Allows to override the default scan consistency. + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param criteria must not be {@literal null}. + * @return new instance of {@link TerminatingFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. */ - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } @@ -188,7 +193,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -198,7 +203,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -233,7 +238,25 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + FindByQueryInCollection inScope(String scope); } /** @@ -241,6 +264,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ReactiveFindByQuery extends FindByQueryWithDistinct {} + interface ReactiveFindByQuery extends FindByQueryInScope {} } 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 42895c998..f18be74bd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.CouchbaseClientFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,9 +27,14 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; /** * {@link ReactiveFindByQueryOperation} implementations for Couchbase. @@ -51,7 +57,7 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, null, null, - template.support()); + null, template.support()); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -69,9 +75,9 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, + final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, + final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -97,14 +103,16 @@ public FindByQueryWithQuery matching(Query query) { scanCons = scanConsistency; } return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields, support); + options, distinctFields, fields, + support); } @Override - public TerminatingFindByQuery withOptions(final QueryOptions options) { + public FindByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, + support); } @Override @@ -114,28 +122,31 @@ public FindByQueryInCollection inScope(final String scope) { } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection != null ? collection : this.collection, options, distinctFields, fields, support); } @Override @Deprecated - public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { + public FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, + support); } @Override public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, + support); } - public FindByQueryWithConsistency as(Class returnType) { + public FindByQueryWithProjecting as(Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, + support); } @Override @@ -143,7 +154,8 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, + support); } @Override @@ -155,7 +167,15 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields, support); + collection, options, dFields, fields, + support); + } + + @Override + public FindByQueryWithTransaction transaction() { + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, + support); } @Override @@ -170,27 +190,37 @@ public Mono first() { @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); - String statement = assembleEntityQuery(false, distinctFields, pArgs.getScope(), pArgs.getCollection()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, + domainType); + String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { + + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + + Mono allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { + if (!s.isPresent()) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), + clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); + } + }); + + return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> { - String id = null; - Long cas = null; - if (query.isDistinct() || distinctFields != null) { - id = ""; - cas = Long.valueOf(0); - } else { + }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() + : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())).flatMap(row -> { + String id = ""; + long cas = 0; + if (!query.isDistinct() && distinctFields == null) { id = row.getString(TemplateUtils.SELECT_ID); if (id == null) { id = row.getString(TemplateUtils.SELECT_ID_3x); @@ -202,10 +232,12 @@ public Flux all() { row.removeKey(TemplateUtils.SELECT_CAS_3x); } row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); - } - return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection()); - })); + row.removeKey(TemplateUtils.SELECT_CAS) + } + System.err.println("row: "+row); + return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), + null); + }); } public QueryOptions buildOptions(QueryOptions options) { @@ -213,24 +245,44 @@ public QueryOptions buildOptions(QueryOptions options) { return query.buildQueryOptions(options, qsc); } + private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { + TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildOptions(options)); + return opts; + } + @Override public Mono count() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); - String statement = assembleEntityQuery(true, distinctFields, pArgs.getScope(), pArgs.getCollection()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, + domainType); + String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono countResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Mono.defer(() -> countResult.onErrorMap(throwable -> { + + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + + Mono allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { + if (!s.isPresent()) { + System.err.println("count: no-tx"); + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + System.err.println("count: tx"); + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), + clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); + } + }); + + return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(row.getNames().iterator().next())) - .next()); + }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() + : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())) + .map(row -> row.getLong(row.getNames().iterator().next())).next(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 0fee3b9e4..28f833f98 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -74,13 +74,14 @@ public Mono any(final String id) { if (garOptions.build().transcoder() == null) { garOptions.transcoder(RawJsonTranscoder.INSTANCE); } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, + domainType); LOG.trace("getAnyReplica key={} {}", id, pArgs); - return Mono.just(id) + return TransactionalSupport.verifyNotInTransaction("findFromReplicasById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, - pArgs.getScope(), pArgs.getCollection())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 6439879fe..70cba57ff 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -27,6 +27,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; @@ -84,17 +85,43 @@ interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertO TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithDurability transaction(); + } + /** * Fluent method to specify the collection. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdTxOrNot extends InsertByIdWithTransaction, InsertByIdWithExpiry {} + + /** + * Fluent method to specify the collection. + */ + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -110,27 +137,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ReactiveInsertById extends InsertByIdWithExpiry {} + interface ReactiveInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 516d41c0a..f56731c8a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,6 +27,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -33,6 +35,8 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -64,8 +68,9 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, ReactiveTemplateSupport support) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -80,21 +85,48 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, + domainType); LOG.trace("insertById object={} {}", object, pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + + return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection())).flatMap(collection -> support.encodeEntity(object) + .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + if (!ctxOpt.isPresent()) { + System.err.println("insert non-tx"); + return collection.reactive() + .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + .flatMap( + result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), null)); + } else { + rejectInvalidTransactionalOptions(); + System.err.println("insert tx"); + return ctxOpt.get().getCore() + .insert(makeCollectionIdentifier(collection.async()), converted.getId(), + template.getCouchbaseClientFactory().getCluster().environment().transcoder() + .encode(converted.export()).encoded()) + .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), + new TransactionResultHolder(result), null)); + } + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })); + } + + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.expiry != null) { + throw new IllegalArgumentException("withExpiry is not supported in a transaction"); + } + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); + } } @Override @@ -110,7 +142,8 @@ public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override @@ -120,7 +153,7 @@ public InsertByIdInCollection inScope(final String scope) { } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); @@ -130,7 +163,8 @@ public InsertByIdWithOptions inCollection(final String collection) { public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override @@ -138,15 +172,25 @@ public InsertByIdInScope withDurability(final PersistTo persistTo, final Repl Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } + + @Override + public InsertByIdWithExpiry transaction() { + return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, + support); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index a9abaa178..42a6211c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -25,6 +25,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -63,6 +64,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { @Override Mono one(String id); + /** + * Remove one document. Requires whole entity for transaction to have the cas. + * + * @param entity the entity + * @return result of the remove + */ + Mono oneEntity(Object entity); /** * Remove the documents in the collection. * @@ -71,6 +79,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { */ @Override Flux all(Collection ids); + /** + * Remove the documents in the collection. Requires whole entity for transaction to have the cas. + * + * @param ids the document IDs. + * @return result of the removes. + */ + Flux allEntities(Collection ids); } @@ -86,22 +101,42 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + @Override + RemoveByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability { + + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { + RemoveByIdWithCas transaction(); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** * Fluent method to specify the scope. */ - interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { + interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { /** * With a different scope * @@ -110,23 +145,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ReactiveRemoveById extends RemoveByIdWithCas {} + interface ReactiveRemoveById extends RemoveByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index d59de09cc..aa20118b4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,6 +15,9 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import org.springframework.data.couchbase.CouchbaseClientFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,10 +30,13 @@ import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -66,8 +72,8 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final Long cas; ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -81,19 +87,58 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, + domainType); LOG.trace("removeById key={} {}", id, pArgs); - return Mono.just(id) - .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) - .map(r -> RemoveResult.from(docId, r))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) + .reactive(); + + return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { + if (!s.isPresent()) { + System.err.println("non-tx remove"); + return rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); + } else { + rejectInvalidTransactionalOptions(); + + System.err.println("tx remove"); + if ( cas == null || cas == 0 ){ + throw new IllegalArgumentException("cas must be supplied for tx remove"); + } + CoreTransactionAttemptContext ctx = s.get().getCore(); + Mono gr = ctx.get(makeCollectionIdentifier(rc.async()), id); + + return gr.flatMap(getResult -> { + if (getResult.cas() != cas) { + return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas)); } + return ctx.remove(getResult) + .map(r -> new RemoveResult(id, 0, null)); }); + + }}).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } + + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); + } + } + + @Override + public Mono oneEntity(Object entity) { + ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, template.support().getCas(entity)); + return op.one(template.support().getId(entity).toString()); } @Override @@ -101,6 +146,11 @@ public Flux all(final Collection ids) { return Flux.fromIterable(ids).flatMap(this::one); } + @Override + public Flux allEntities(Collection entities) { + return Flux.fromIterable(entities).flatMap(this::oneEntity); + } + private RemoveOptions buildRemoveOptions(RemoveOptions options) { return OptionsBuilder.buildRemoveOptions(options, persistTo, replicateTo, durabilityLevel, cas); } @@ -144,6 +194,13 @@ public RemoveByIdWithDurability withCas(Long cas) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } + + @Override + public RemoveByIdWithCas transaction() { + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 7619eabf1..3388dd648 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -54,31 +55,61 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { + + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + @Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { /** - * Fluent method to specify options to use for execution + * Provide the transaction * - * @param options to use for execution */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + @Override + TerminatingRemoveByQuery transaction(); + } + + interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryTxOrNot, WithQuery { + + RemoveByQueryTxOrNot matching(Query query); + + default RemoveByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** @@ -86,13 +117,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -109,25 +140,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ReactiveRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ReactiveRemoveByQuery extends RemoveByQueryInScope {} } 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 4358294bb..e11ac7b8d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,8 +15,12 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.util.Optional; @@ -28,6 +32,7 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; @@ -60,7 +65,7 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final QueryOptions options; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { this.template = template; this.domainType = domainType; this.query = query; @@ -75,20 +80,29 @@ public Flux all() { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); String statement = assembleDeleteQuery(pArgs.getScope(), pArgs.getCollection()); LOG.trace("removeByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject) - .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), - Optional.empty()))); + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + + return TransactionalSupport.checkForTransactionInThreadLocalStorage() + .flatMapMany(transactionContext -> { + + if (!transactionContext.isPresent()) { + QueryOptions opts = buildQueryOptions(pArgs.getOptions()); + return (pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts)).flatMapMany(ReactiveQueryResult::rowsAsObject) + .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), + row.getLong(TemplateUtils.SELECT_CAS), Optional.empty())); + } else { + TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildQueryOptions(pArgs.getOptions())); + ObjectNode convertedOptions = com.couchbase.client.java.transactions.internal.OptionsUtil.createTransactionOptions(pArgs.getScope() == null ? null : rs, statement, opts); + return transactionContext.get().getCore().queryBlocking(statement, template.getBucketName(), pArgs.getScope(), convertedOptions, false) + .flatMapIterable(result -> result.rows).map(row -> { + JsonObject json = JsonObject.fromJson(row.data()); + return new RemoveResult(json.getString(TemplateUtils.SELECT_ID), + json.getLong(TemplateUtils.SELECT_CAS), Optional.empty()); + }); + } + }); } private QueryOptions buildQueryOptions(QueryOptions options) { @@ -97,13 +111,13 @@ private QueryOptions buildQueryOptions(QueryOptions options) { } @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryTxOrNot matching(final Query query) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @Override - public RemoveByQueryWithConsistency inCollection(final String collection) { + public RemoveByQueryWithQuery inCollection(final String collection) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection != null ? collection : this.collection, options); } @@ -138,6 +152,13 @@ public RemoveByQueryInCollection inScope(final String scope) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope != null ? scope : this.scope, collection, options); } + + @Override + public RemoveByQueryWithConsistency transaction() { + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 0341096cb..d78f40269 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -27,6 +27,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -84,19 +85,39 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + + ReplaceByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + ReplaceByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + @Override + ReplaceByIdWithExpiry transaction(); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -114,24 +135,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ReactiveReplaceById extends ReplaceByIdWithExpiry {} + interface ReactiveReplaceById extends ReplaceByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 246d76f46..00e76110d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -15,6 +15,10 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.core.transaction.util.DebugUtil; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,6 +37,8 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -64,8 +70,9 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReactiveTemplateSupport support; ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { + final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -80,21 +87,60 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, + domainType); LOG.trace("replaceById object={} {}", object, pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyUpdatedCas(object, converted, result.cas()))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + + return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection())).flatMap(collection -> support.encodeEntity(object) + .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + if (!ctxOpt.isPresent()) { + System.err.println("replace: non-tx"); + return collection.reactive() + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null)); + } else { + System.err.println("replace: tx"); + rejectInvalidTransactionalOptions(); + + Long cas = support.getCas(object); + if ( cas == null || cas == 0 ){ + throw new IllegalArgumentException("cas must be supplied in object for tx replace. object="+object); + } + + CollectionIdentifier collId = makeCollectionIdentifier(collection.async()); + CoreTransactionAttemptContext ctx = ctxOpt.get().getCore(); + ctx.logger().info(ctx.attemptId(), "refetching %s for Spring replace", DebugUtil.docId(collId, converted.getId())); + Mono gr = ctx.get(collId, converted.getId()); + + return gr.flatMap(getResult -> { + if (getResult.cas() != cas) { + return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas)); + } + return ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().environment().transcoder() + .encode(converted.export()).encoded()); + }).flatMap(result -> this.support.applyResult(object, converted, converted.getId(), 0L, null, null)); + } + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })); + } + + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.expiry != null) { + throw new IllegalArgumentException("withExpiry is not supported in a transaction"); + } + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); + } } @Override @@ -111,7 +157,8 @@ private ReplaceOptions buildReplaceOptions(ReplaceOptions options, T object, Cou public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override @@ -131,7 +178,8 @@ public ReplaceByIdInCollection inScope(final String scope) { public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override @@ -139,14 +187,23 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, + support); + } + + @Override + public ReplaceByIdWithExpiry transaction() { + return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, + support); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 69d5db015..770df7f73 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -17,8 +17,10 @@ import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** * @author Michael Reiche @@ -27,15 +29,23 @@ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder); - Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); - Mono applyUpdatedId(T entity, Object id); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder); + + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); Long getCas(Object entity); + Object getId(Object entity); + String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index 05249e198..b3045a25b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -86,19 +86,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -116,25 +129,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ReactiveUpsertById extends UpsertByIdWithExpiry {} + interface ReactiveUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 997605983..aaae1a370 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -64,8 +64,8 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { private final ReactiveTemplateSupport support; ReactiveUpsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { + final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -80,21 +80,25 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, + domainType); LOG.trace("upsertById object={} {}", object, pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } + Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction("upsertById") + .then(support.encodeEntity(object)) + .flatMap(converted -> { + return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection())).flatMap(collection -> collection.reactive() + .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); }); + + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index bd5ab0ae9..df50cd41b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -15,8 +15,11 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** * @@ -26,15 +29,23 @@ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); - T applyUpdatedId(T entity, Object id); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); - long getCas(Object entity); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); + + Long getCas(Object entity); + + Object getId(Object entity); String getJavaNameForEntity(Class clazz); void maybeEmitEvent(CouchbaseMappingEvent event); + + Integer getTxResultHolder(T source); + + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java new file mode 100644 index 000000000..631249838 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java @@ -0,0 +1,57 @@ +package org.springframework.data.couchbase.core; + +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.threadlocal.TransactionMarkerOwner; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import reactor.core.publisher.Mono; + +import java.util.Optional; + +import com.couchbase.client.core.annotation.Stability; + +@Stability.Internal +public class TransactionalSupport { + + /** + * Returns non-empty iff in a transaction. It determines this from thread-local storage and/or reactive context. + *

+ * The user could be doing a reactive operation (with .block()) inside a blocking transaction (like @Transactional). + * Or a blocking operation inside a ReactiveTransactionsWrapper transaction (which would be a bad idea). + * So, need to check both thread-local storage and reactive context. + */ + public static Mono> checkForTransactionInThreadLocalStorage() { + return TransactionMarkerOwner.get().flatMap(markerOpt -> { + Optional out = markerOpt + .flatMap(marker -> Optional.of(new CouchbaseResourceHolder(marker.context()))); + return Mono.just(out); + }); + } + + public static Mono verifyNotInTransaction(String methodName) { + return checkForTransactionInThreadLocalStorage() + .flatMap(s -> { + if (s.isPresent()) { + return Mono.error(new IllegalArgumentException(methodName + "can not be used inside a transaction")); + } + else { + return Mono.empty(); + } + }); + } + + public static RuntimeException retryTransactionOnCasMismatch(CoreTransactionAttemptContext ctx, long cas1, long cas2) { + try { + ctx.logger().info(ctx.attemptId(), "Spring CAS mismatch %s != %s, retrying transaction", cas1, cas2); + TransactionOperationFailedException err = TransactionOperationFailedException.Builder.createError() + .retryTransaction() + .cause(new CasMismatchException(null)) + .build(); + return ctx.operationFailed(err); + } catch (Throwable err) { + return new RuntimeException(err); + } + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index e480f9c6d..b90756dcd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -730,7 +730,6 @@ private CouchbaseList writeCollectionInternal(final Collection source, final target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()), componentType)); } else { - CouchbaseDocument embeddedDoc = new CouchbaseDocument(); writeInternal(element, embeddedDoc, componentType, false); target.put(embeddedDoc); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index cf4a42411..0a4efc58a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -70,6 +70,7 @@ public void setEnvironment(Environment environment) { @Override protected CouchbasePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull( CouchbasePersistentProperty property) { + if (!property.isIdProperty()) { return null; } @@ -164,15 +165,4 @@ public boolean isTouchOnRead() { .getAnnotation(org.springframework.data.couchbase.core.mapping.Document.class); return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } - - @Override - public boolean hasTextScoreProperty() { - return false; - } - - @Override - public CouchbasePersistentProperty getTextScoreProperty() { - return null; - } - } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index 17a53cd3e..3ae142faa 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -61,8 +61,4 @@ public interface CouchbasePersistentEntity extends PersistentEntityReactiveAfterConvertCallback * * @author Jorge Rodríguez Martín * @authoer Michael Reiche diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java index 20fe310d5..159826498 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -24,8 +24,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.Duration; +import java.util.Map; import java.util.Optional; +import com.couchbase.client.java.transactions.TransactionQueryOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -70,7 +72,7 @@ static QueryOptions buildQueryOptions(Query query, QueryOptions options, QuerySc QueryScanConsistency metaQueryScanConsistency = meta.get(SCAN_CONSISTENCY) != null ? ((ScanConsistency) meta.get(SCAN_CONSISTENCY)).query() : null; - QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, getScanConsistency(optsJson), + QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, query.getScanConsistency(), getScanConsistency(optsJson), scanConsistency, metaQueryScanConsistency); Duration timeout = fromFirst(Duration.ofSeconds(0), getTimeout(optsBuilt), meta.get(TIMEOUT)); RetryStrategy retryStrategy = fromFirst(null, getRetryStrategy(optsBuilt), meta.get(RETRY_STRATEGY)); @@ -90,6 +92,26 @@ static QueryOptions buildQueryOptions(Query query, QueryOptions options, QuerySc return options; } + public static TransactionQueryOptions buildTransactionQueryOptions(QueryOptions options) { + QueryOptions.Built built = options.build(); + TransactionQueryOptions txOptions = TransactionQueryOptions.queryOptions(); + + JsonObject optsJson = getQueryOpts(built); + + if (optsJson.containsKey("use_fts")) { + throw new IllegalArgumentException("QueryOptions.flexIndex is not supported in a transaction"); + } + + for (Map.Entry entry : optsJson.toMap().entrySet()) { + txOptions.raw(entry.getKey(), entry.getValue()); + } + + if (LOG.isTraceEnabled()) { + LOG.trace("query options: {}", optsJson); + } + return txOptions; + } + public static ExistsOptions buildExistsOptions(ExistsOptions options) { options = options != null ? options : ExistsOptions.existsOptions(); return options; @@ -423,4 +445,5 @@ public static String annotationString(Class annotation AnnotatedElement[] elements) { return annotationString(annotation, "value", defaultValue, elements); } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index cb9ac9c92..7edad7d5e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -17,6 +17,7 @@ import static org.springframework.data.couchbase.core.query.OptionsBuilder.fromFirst; +import com.couchbase.client.core.error.CouchbaseException; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import com.couchbase.client.core.io.CollectionIdentifier; @@ -44,15 +45,14 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * 1) values from fluent api
* 2) values from dynamic proxy (via template threadLocal)
* 3) the values from the couchbaseClientFactory
- * - * @param template which holds ThreadLocal pseudo args + * @param template which holds ThreadLocal pseudo args * @param scope - from calling operation * @param collection - from calling operation * @param options - from calling operation * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - Class domainType) { + Class domainType) { String scopeForQuery = null; String collectionForQuery = null; @@ -96,7 +96,7 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle // if a collection was specified but no scope, use the scope from the clientFactory if (collectionForQuery != null && scopeForQuery == null) { - scopeForQuery = template.getCouchbaseClientFactory().getScope().name(); + scopeForQuery = template.getScopeName(); } // specifying scope and collection = _default is not necessary and will fail if server doesn't have collections @@ -110,6 +110,9 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; + if( scopeForQuery != null && collectionForQuery == null){ + throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); + } this.options = optionsForQuery; } @@ -139,4 +142,5 @@ public String getCollection() { public String toString() { return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions(); } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java new file mode 100644 index 000000000..1399235a3 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 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.support; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * A common interface operations that take scan consistency + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithCas { + /** + * Specify scan consistency + * + * @param cas + */ + Object withCas(Long cas); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java new file mode 100644 index 000000000..d86691ac9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 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.support; + +/** + * Interface for operations that take distinct fields + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithTransaction { + /** + * Specify transactions + * + */ + Object transaction(); +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index 01e56fb32..9b83d9399 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -27,7 +27,7 @@ * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a * ReactiveCouchbaseRepository parameterized on T,ID. i.e.: interface AirportRepository extends * CouchbaseRepository<Airport, String>, DynamicProxyable<AirportRepository> - * + * * @param * @author Michael Reiche */ @@ -38,24 +38,32 @@ public interface DynamicProxyable { Object getOperations(); /** - * Support for Couchbase-specific options, scope and collections The three "with" methods will return a new proxy - * instance with the specified options, scope, or collections set. The setters are called with the corresponding - * options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the repository just - * before the call is made to the repository, and called again with 'null' just after the call is made. The repository - * method will fetch those values to use in the call. + * Support for Couchbase-specific options, scope and collections The four "with" methods will return a new proxy + * instance with the specified options, scope, collection or transactionalOperator set. The setters are called with + * the corresponding options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the + * repository just before the call is made to the repository, and called again with 'null' just after the call is + * made. The repository method will fetch those values to use in the call. */ /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param options - the options to set on the returned repository object */ @SuppressWarnings("unchecked") default REPO withOptions(CommonOptions options) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, (String) null)); + this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, null)); return proxyInstance; } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param scope - the scope to set on the returned repository object */ @SuppressWarnings("unchecked") @@ -66,6 +74,10 @@ default REPO withScope(String scope) { } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param collection - the collection to set on the returned repository object */ @SuppressWarnings("unchecked") diff --git a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index d805a6c8a..4ca88045b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -34,4 +34,5 @@ public interface ReactiveCouchbaseRepository ReactiveCouchbaseOperations getOperations(); CouchbaseEntityInformation getEntityInformation(); + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index 777aa0d6b..31775fb2d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -37,7 +37,7 @@ public class CouchbaseRepositoryBase { private CrudMethodMetadata crudMethodMetadata; public CouchbaseRepositoryBase(CouchbaseEntityInformation entityInformation, - Class repositoryInterface) { + Class repositoryInterface) { this.entityInformation = entityInformation; this.repositoryInterface = repositoryInterface; } @@ -55,12 +55,21 @@ public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } + /** + * Returns the repository interface + * + * @return the underlying entity information. + */ + public Class getRepositoryInterface() { + return repositoryInterface; + } + Class getJavaType() { return getEntityInformation().getJavaType(); } String getId(S entity) { - return getEntityInformation().getId(entity); + return String.valueOf(getEntityInformation().getId(entity)); } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index dedba009d..cde109dcf 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -44,7 +44,7 @@ public class DynamicInvocationHandler implements InvocationHandler { final ReactiveCouchbaseTemplate reactiveTemplate; CommonOptions options; String collection; - String scope;; + String scope; public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope) { this.target = target; @@ -52,9 +52,12 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); this.entityInformation = ((CouchbaseRepository) target).getEntityInformation(); } else if (target instanceof ReactiveCouchbaseRepository) { - reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations(); - this.entityInformation = ((ReactiveCouchbaseRepository) target).getEntityInformation(); + reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) this.target).getOperations(); + this.entityInformation = ((ReactiveCouchbaseRepository) this.target).getEntityInformation(); } else { + if( CouchbaseRepository.class.isAssignableFrom(target.getClass())) + System.err.println("isAssignable"); + printInterfaces(target.getClass(), " "); throw new RuntimeException("Unknown target type: " + target.getClass()); } this.options = options; @@ -63,6 +66,15 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle this.repositoryClass = target.getClass(); } + void printInterfaces(Class clazz, String tab){ + System.out.println(tab+"{"); + for(Class c:clazz.getInterfaces()){ + System.out.println(tab+" " +c.getSimpleName()); + if(c.getInterfaces().length > 0) + printInterfaces(c, tab+" "); + } + System.out.println(tab+"}"); + } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index d986fd747..1469e6ee5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -63,7 +63,7 @@ public class SimpleReactiveCouchbaseRepository extends CouchbaseRepositor * @param operations the reference to the reactive template used. */ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation entityInformation, - ReactiveCouchbaseOperations operations, Class repositoryInterface) { + ReactiveCouchbaseOperations operations, Class repositoryInterface) { super(entityInformation, repositoryInterface); this.operations = operations; } @@ -214,7 +214,7 @@ public Mono deleteAllById(Iterable ids) { @Override public Mono deleteAll(Iterable entities) { return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) - .all(Streamable.of(entities).map(this::getId).toList()).then(); + .allEntities((java.util.Collection)(Streamable.of(entities).toList())).then(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java new file mode 100644 index 000000000..653bba57f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 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.support; + +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import reactor.util.annotation.Nullable; + +/** + * Holds previously obtained Transaction*Result + * + * @author Michael Reiche + */ +public class TransactionResultHolder { + + private final @Nullable CoreTransactionGetResult getResult; + // todo gp needed? + private final @Nullable QueryResult singleQueryResult; + + public TransactionResultHolder(CoreTransactionGetResult getResult) { + // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). + // todo gp will need to expose a copy ctor if a copy is needed + this.getResult = getResult; +// this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), +// getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); + this.singleQueryResult = null; + } + + public TransactionResultHolder(QueryResult singleQueryResult) { + this.getResult = null; + this.singleQueryResult = singleQueryResult; + } + + public @Nullable CoreTransactionGetResult transactionGetResult() { + return getResult; + } + + public @Nullable QueryResult singleQueryResult() { + return singleQueryResult; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java new file mode 100644 index 000000000..427a3d3e4 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -0,0 +1,285 @@ +/* + * Copyright 2021 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.transaction; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.TransactionalSupport; +import org.springframework.data.couchbase.transaction.error.TransactionRollbackRequestedException; +import org.springframework.data.couchbase.transaction.error.TransactionSystemAmbiguousException; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.lang.Nullable; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.TransactionCallback; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * The Couchbase transaction manager, providing support for @Transactional methods. + */ +public class CouchbaseCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseCallbackTransactionManager.class); + + private final CouchbaseClientFactory couchbaseClientFactory; + private @Nullable TransactionOptions options; + + public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory) { + this(couchbaseClientFactory, null); + } + + /** + * This override is for users manually creating a CouchbaseCallbackTransactionManager, and allows the + * TransactionOptions to be overridden. + */ + public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory, @Nullable TransactionOptions options) { + this.couchbaseClientFactory = couchbaseClientFactory; + this.options = options != null ? options : TransactionOptions.transactionOptions(); + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + boolean createNewTransaction = handlePropagation(definition); + + setOptionsFromDefinition(definition); + + if (createNewTransaction) { + return executeNewTransaction(callback); + } else { + return callback.doInTransaction(null); + } + } + + @Stability.Internal + Flux executeReactive(TransactionDefinition definition, + org.springframework.transaction.reactive.TransactionCallback callback) { + return Flux.defer(() -> { + boolean createNewTransaction = handlePropagation(definition); + + setOptionsFromDefinition(definition); + + if (createNewTransaction) { + return executeNewReactiveTransaction(callback); + } else { + return Mono.error(new UnsupportedOperationException("Unsupported operation")); + } + }); + } + + private T executeNewTransaction(TransactionCallback callback) { + final AtomicReference execResult = new AtomicReference<>(); + + // Each of these transactions will block one thread on the underlying SDK's transactions scheduler. This + // scheduler is effectively unlimited, but this can still potentially lead to high thread usage by the application. + // If this is an issue then users need to instead use the standard Couchbase reactive transactions SDK. + try { + TransactionResult ignored = couchbaseClientFactory.getCluster().transactions().run(ctx -> { + CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(ctx, true, false, false, true, null); + + T res = callback.doInTransaction(status); + if (res instanceof Mono || res instanceof Flux) { + throw new UnsupportedOperationException("Return type is Mono or Flux, indicating a reactive transaction is being performed in a blocking way. A potential cause is the CouchbaseTransactionInterceptor is not in use."); + } + execResult.set(res); + + if (status.isRollbackOnly()) { + throw new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set"); + } + }, this.options); + + return execResult.get(); + } + catch (RuntimeException ex) { + throw convert(ex); + } + } + + private static RuntimeException convert(RuntimeException ex) { + if (ex instanceof TransactionCommitAmbiguousException) { + return new TransactionSystemAmbiguousException((TransactionCommitAmbiguousException) ex); + } + if (ex instanceof TransactionFailedException) { + return new TransactionSystemUnambiguousException((TransactionFailedException) ex); + } + // Should not get here + return ex; + } + + private Flux executeNewReactiveTransaction(org.springframework.transaction.reactive.TransactionCallback callback) { + // Buffer the output rather than attempting to stream results back from a now-defunct lambda. + final List out = new ArrayList<>(); + + return couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + return Mono.defer(() -> { + ReactiveTransaction status = new ReactiveTransaction() { + boolean rollbackOnly = false; + + @Override + public boolean isNewTransaction() { + return true; + } + + @Override + public void setRollbackOnly() { + this.rollbackOnly = true; + } + + @Override + public boolean isRollbackOnly() { + return rollbackOnly; + } + + @Override + public boolean isCompleted() { + return false; + } + }; + + return Flux.from(callback.doInTransaction(status)) + .doOnNext(v -> out.add(v)) + .then(Mono.defer(() -> { + if (status.isRollbackOnly()) { + return Mono + .error(new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set")); + } + return Mono.empty(); + })); + }); + + }, this.options) + .thenMany(Flux.defer(() -> Flux.fromIterable(out))) + .onErrorMap(ex -> { + if (ex instanceof RuntimeException) { + return convert((RuntimeException) ex); + } + return ex; + }); + } + + // Propagation defines what happens when a @Transactional method is called from another @Transactional method. + private boolean handlePropagation(TransactionDefinition definition) { + boolean isExistingTransaction = TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent(); + + LOGGER.trace("Deciding propagation behaviour from {} and {}", definition.getPropagationBehavior(), + isExistingTransaction); + + switch (definition.getPropagationBehavior()) { + case TransactionDefinition.PROPAGATION_REQUIRED: + // Make a new transaction if required, else just execute the new method in the current transaction. + return !isExistingTransaction; + + case TransactionDefinition.PROPAGATION_SUPPORTS: + // Don't appear to have the ability to execute the callback non-transactionally in this layer. + throw new UnsupportedOperationException( + "Propagation level 'support' has been specified which is not supported"); + + case TransactionDefinition.PROPAGATION_MANDATORY: + if (!isExistingTransaction) { + throw new IllegalTransactionStateException( + "Propagation level 'mandatory' is specified but not in an active transaction"); + } + return false; + + case TransactionDefinition.PROPAGATION_REQUIRES_NEW: + // This requires suspension of the active transaction. This will be possible to support in a future + // release, if required. + throw new UnsupportedOperationException( + "Propagation level 'requires_new' has been specified which is not currently supported"); + + case TransactionDefinition.PROPAGATION_NOT_SUPPORTED: + // Don't appear to have the ability to execute the callback non-transactionally in this layer. + throw new UnsupportedOperationException( + "Propagation level 'not_supported' has been specified which is not supported"); + + case TransactionDefinition.PROPAGATION_NEVER: + if (isExistingTransaction) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } + return true; + + case TransactionDefinition.PROPAGATION_NESTED: + if (isExistingTransaction) { + // Couchbase transactions cannot be nested. + throw new UnsupportedOperationException( + "Propagation level 'nested' has been specified which is not supported"); + } + return true; + + default: + throw new UnsupportedOperationException( + "Unknown propagation level " + definition.getPropagationBehavior() + " has been specified"); + } + } + + /** + * @param definition reflects the @Transactional options + */ + private void setOptionsFromDefinition(TransactionDefinition definition) { + if (definition != null) { + if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { + if (options == null) { + options = TransactionOptions.transactionOptions(); + } + options = options.timeout(Duration.ofSeconds(definition.getTimeout())); + } + + if (!(definition.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT + || definition.getIsolationLevel() == TransactionDefinition.ISOLATION_READ_COMMITTED)) { + throw new IllegalArgumentException( + "Couchbase Transactions run at Read Committed isolation - other isolation levels are not supported"); + } + + // readonly is ignored as it is documented as being a hint that won't necessarily cause writes to fail + } + + } + + @Override + public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // All Spring transactional code (currently) does not call the getTransaction, commit or rollback methods if + // the transaction manager is a CallbackPreferringPlatformTransactionManager. + // So these methods should only be hit if user is using PlatformTransactionManager directly. Spring supports this, + // but due to the lambda-based nature of our transactions, we cannot. + throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + } + + @Override + public void commit(TransactionStatus ignored) throws TransactionException { + throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + } + + @Override + public void rollback(TransactionStatus ignored) throws TransactionException { + throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java new file mode 100644 index 000000000..68f1030c6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2021 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.transaction; + +import com.couchbase.client.core.annotation.Stability; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; + +import java.util.HashMap; +import java.util.Map; + +@Stability.Internal +public class CouchbaseResourceHolder extends ResourceHolderSupport { + + private @Nullable CoreTransactionAttemptContext core; // which holds the atr + Map getResultMap = new HashMap<>(); + + /** + * Create a new {@link CouchbaseResourceHolder} for a given {@link CoreTransactionAttemptContext session}. + * + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + public CouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) { + + this.core = core; + } + + /** + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + @Nullable + public CoreTransactionAttemptContext getCore() { + return core; + } + + public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { + System.err.println("PUT: "+System.identityHashCode(o)+" "+o); + getResultMap.put(System.identityHashCode(o), holder); + return holder; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java new file mode 100644 index 000000000..6bb5936b8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -0,0 +1,12 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.annotation.Stability; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +@Stability.Internal +public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition { + public CouchbaseTransactionDefinition(){ + super(); + setIsolationLevel(ISOLATION_READ_COMMITTED); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java new file mode 100644 index 000000000..df613f3db --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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.transaction; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.couchbase.client.core.annotation.Stability; +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * This allows reactive @Transactional support with Couchbase transactions. + *

+ * The ReactiveTransactionManager does not support the lambda-based nature of Couchbase transactions, + * and there is no reactive equivalent of CallbackPreferringTransactionManager (which does). + *

+ * The solution: override the standard TransactionInterceptor and, if the + * CouchbaseCallbackTransactionManager is the provided TransactionManager, defer to that. + */ +@Stability.Internal +public class CouchbaseTransactionInterceptor extends TransactionInterceptor + implements MethodInterceptor, Serializable { + + public CouchbaseTransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) { + super(ptm, tas); + } + + @Nullable + protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + final TransactionAttributeSource tas = getTransactionAttributeSource(); + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + + if (getTransactionManager() instanceof CouchbaseCallbackTransactionManager) { + CouchbaseCallbackTransactionManager manager = (CouchbaseCallbackTransactionManager) getTransactionManager(); + + if (Mono.class.isAssignableFrom(method.getReturnType())) { + return manager.executeReactive(txAttr, ignored -> { + try { + return (Mono) invocation.proceedWithInvocation(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + }).singleOrEmpty(); + } else if (Flux.class.isAssignableFrom(method.getReturnType())) { + return manager.executeReactive(txAttr, ignored -> { + try { + return (Flux) invocation.proceedWithInvocation(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } else { + return manager.execute(txAttr, ignored -> { + try { + return invocation.proceedWithInvocation(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + } else { + return super.invokeWithinTransaction(method, targetClass, invocation); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java new file mode 100644 index 000000000..516c2fcec --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java @@ -0,0 +1,30 @@ +package org.springframework.data.couchbase.transaction; + +import org.springframework.transaction.support.DefaultTransactionStatus; + +public class CouchbaseTransactionStatus extends DefaultTransactionStatus { + + /** + * Create a new {@code DefaultTransactionStatus} instance. + * + * @param transaction underlying transaction object that can hold state + * for the internal transaction implementation + * @param newTransaction if the transaction is new, otherwise participating + * in an existing transaction + * @param newSynchronization if a new transaction synchronization has been + * opened for the given transaction + * @param readOnly whether the transaction is marked as read-only + * @param debug should debug logging be enabled for the handling of this transaction? + * Caching it in here can prevent repeated calls to ask the logging system whether + * debug logging should be enabled. + * @param suspendedResources a holder for resources that have been suspended + */ + public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources) { + super(transaction, + newTransaction, + newSynchronization, + readOnly, + debug, + suspendedResources); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java new file mode 100644 index 000000000..d1a06e479 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 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.transaction; + +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.reactive.TransactionCallback; +import org.springframework.transaction.reactive.TransactionalOperator; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * The TransactionalOperator interface is another method to perform reactive transactions with Spring. + *

+ * We recommend instead using a regular reactive SDK transaction, and performing Spring operations inside it. + */ +public class CouchbaseTransactionalOperator implements TransactionalOperator { + private final CouchbaseCallbackTransactionManager manager; + + CouchbaseTransactionalOperator(CouchbaseCallbackTransactionManager manager) { + this.manager = manager; + } + + public static CouchbaseTransactionalOperator create(CouchbaseCallbackTransactionManager manager) { + return new CouchbaseTransactionalOperator(manager); + } + + @Override + public Mono transactional(Mono mono) { + return transactional(Flux.from(mono)) + .singleOrEmpty(); + } + + @Override + public Flux execute(TransactionCallback action) throws TransactionException { + return Flux.defer(() -> { + TransactionDefinition def = new CouchbaseTransactionDefinition(); + return manager.executeReactive(def, action); + }); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java new file mode 100644 index 000000000..05841b319 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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.transaction.error; + +import com.couchbase.client.core.error.CouchbaseException; + +/** + * A transaction rollback has been requested. + */ +public class TransactionRollbackRequestedException extends CouchbaseException { + public TransactionRollbackRequestedException(String message) { + super(message); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java new file mode 100644 index 000000000..2053e8a55 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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.transaction.error; + +import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException; + +/** + * The transaction expired at the point of trying to commit it. It is ambiguous whether the transaction has committed + * or not. Actors may be able to see the content of this transaction. + * + * This error is the result of inevitable and unavoidable edge cases when working with unreliable networks. For example, + * consider an ordinary mutation being made over the network to any database. The mutation could succeed on the + * database-side, and then just before the result is returned to the client, the network connection drops. The client + * cannot receive the success result and will timeout - it is ambiguous to it whether the mutation succeeded or not. + * + * The transactions logic will work to resolve the ambiguity up until the transaction expires, but if unable to resolve + * it in that time, it is forced to raise this error. The transaction may or may not have been successful, and + * error-handling of this is highly application-dependent. + * + * An asynchronous cleanup process will try to complete the transaction: roll it back if it didn't commit, roll it + * forwards if it did. + */ +public class TransactionSystemAmbiguousException extends TransactionSystemCouchbaseException { + public TransactionSystemAmbiguousException(TransactionCommitAmbiguousException ex) { + super(ex); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java new file mode 100644 index 000000000..367cacac1 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 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.transaction.error; + +import java.util.List; + +import com.couchbase.client.core.cnc.events.transaction.TransactionLogEvent; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.springframework.transaction.TransactionSystemException; + +/** + * A base class of transaction-level exceptions raised by Couchbase, allowing them to be handled in + * one place. + */ +abstract public class TransactionSystemCouchbaseException extends TransactionSystemException { + private final TransactionFailedException internal; + + public TransactionSystemCouchbaseException(TransactionFailedException ex) { + super(ex.getMessage(), ex.getCause()); + this.internal = ex; + } + + /** + * An in-memory log is built up during each transaction. The application may want to write this to their own logs, + * for example upon transaction failure. + */ + public List logs() { + return internal.logs(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java new file mode 100644 index 000000000..676da52eb --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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.transaction.error; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * The transaction failed and unambiguously did not commit. No actors can see any part of this failed + * transaction. + *

+ * The application does not need to do anything to rollback the transaction. + */ +public class TransactionSystemUnambiguousException extends TransactionSystemCouchbaseException { + public TransactionSystemUnambiguousException(TransactionFailedException ex) { + super(ex); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java new file mode 100644 index 000000000..5d65acab6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 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.transaction.error; + +import org.springframework.dao.UncategorizedDataAccessException; + +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException; + +/** + * An opaque signal that something went wrong during the execution of an operation inside a transaction. + *

+ * The application is not expected to catch or inspect this exception, and should allow it to propagate. + *

+ * Internal state has been set that ensures that the transaction will act appropriately (including rolling + * back and retrying if necessary) regardless of what the application does with this exception. + */ +public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException implements WrappedTransactionOperationFailedException { + private final TransactionOperationFailedException internal; + + public UncategorizedTransactionDataAccessException(TransactionOperationFailedException err) { + super(err.getMessage(), err.getCause()); + this.internal = err; + } + + @Override + public TransactionOperationFailedException wrapped() { + return internal; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index ec6026ca7..af25691ee 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -18,10 +18,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.util.UUID; diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index f34df744d..8543ed78e 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -26,14 +26,13 @@ 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.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.query.QueryOptions; @@ -43,6 +42,7 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseCacheIntegrationTests extends JavaIntegrationTests { volatile CouchbaseCache cache; diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 2e9f14d1a..1adcd7ff9 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -35,8 +35,10 @@ import java.util.Set; import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; @@ -48,6 +50,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.Submission; @@ -56,6 +59,7 @@ import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.transactions.CouchbaseReactiveTransactionNativeTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -65,6 +69,7 @@ import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; ; @@ -75,12 +80,15 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { - super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 4c8814f7e..1dedbad5c 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -36,11 +37,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -54,9 +57,11 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; +import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.kv.ExistsOptions; @@ -67,7 +72,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -77,8 +81,12 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "loww"); @BeforeAll @@ -103,18 +111,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); } @AfterEach @@ -123,8 +129,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -137,8 +142,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -180,8 +185,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -205,8 +210,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -222,17 +227,17 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); } @@ -247,8 +252,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -272,8 +277,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -296,18 +301,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -315,8 +320,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -340,26 +345,29 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); - // count (distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + // count( distinct (all fields in icaoClass) // which only has one field + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Class icaoClass = (new Object() { + String icao; + }).getClass(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -432,9 +440,8 @@ public void findByQuery() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("441")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()); @@ -485,9 +492,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("495")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -576,9 +583,8 @@ public void findByQueryOther() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("594")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()); @@ -629,9 +635,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("648")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -694,9 +700,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all()); + assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all()); } @Test @@ -734,9 +739,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all()); + () -> couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all()); } @Test @@ -760,9 +765,8 @@ public void testScopeCollectionAnnotation() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); @@ -781,9 +785,8 @@ public void testScopeCollectionRepoWith() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); @@ -801,7 +804,7 @@ void testFluentApi() { DurabilityLevel dl = DurabilityLevel.NONE; User result; RemoveResult rr; - result = couchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = couchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); assertEquals(user1, result); result = couchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index e6e749f12..691f0dc8a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -33,11 +33,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AssessmentDO; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -53,6 +55,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Query tests Theses tests rely on a cb server running @@ -63,8 +66,13 @@ * @author Mauro Monti */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { @@ -129,8 +137,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).matching(specialUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -144,7 +152,7 @@ void findAssessmentDO() { Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals("123", foundUsers.get(0).getId(), "id"); assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); assertEquals(ado, foundUsers.get(0)); @@ -172,7 +180,7 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).matching(daveUsers).all(); + .as(UserSubmissionProjected.class).matching(daveUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -189,11 +197,11 @@ void findByMatchingQueryProjected() { Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).matching(specialUsers).all().collectList().block(); + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(1, foundUsersReactive.size()); couchbaseTemplate.removeById(User.class).all(Arrays.asList(user1.getId(), user2.getId(), specialUser.getId())); @@ -230,7 +238,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where(i("firstname")).notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).matching(nonSpecialUsers).withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); @@ -267,6 +275,15 @@ void distinct() { .as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); + // count( distinct (all fields in icaoClass) + Class icaoClass = (new Object() { + String iata; + String icao; + }).getClass(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).count(); + assertEquals(7, count2); + } finally { couchbaseTemplate.removeById() .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); @@ -298,7 +315,8 @@ void distinctReactive() { assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) .as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); @@ -328,8 +346,8 @@ void sortedTemplate() { .query(QueryCriteria.where("iata").isNotNull()); Pageable pageableWithSort = PageRequest.of(0, 7, Sort.by("iata")); query.with(pageableWithSort); - List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) - .matching(query).all(); + List airports = couchbaseTemplate.findByQuery(Airport.class).matching(query).withConsistency(REQUEST_PLUS) + .all(); String[] sortedIatas = iatas.clone(); System.out.println("" + iatas.length + " " + sortedIatas.length); diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index 8fd709ec5..265ad44df 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -37,6 +37,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ReactiveFindByIdOperation.ReactiveFindById; @@ -46,6 +48,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; import org.springframework.data.couchbase.domain.User; @@ -59,6 +62,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * KV tests Theses tests rely on a cb server running. @@ -67,8 +71,12 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 42edc672a..26dafb8a0 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core.query; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -36,10 +37,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -52,6 +56,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; @@ -64,7 +69,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -74,10 +78,15 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "low80"); - ReactiveCouchbaseTemplate template = reactiveCouchbaseTemplate; + ReactiveCouchbaseTemplate template; @BeforeAll public static void beforeAll() { @@ -101,18 +110,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + + template = reactiveCouchbaseTemplate; } @AfterEach @@ -121,8 +128,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -135,8 +141,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -178,8 +184,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -203,8 +209,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -220,13 +226,13 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); } @@ -242,8 +248,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -267,8 +273,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -291,18 +297,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -310,8 +316,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -335,26 +341,25 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); // count( distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] { "iata", "icao" }) + .withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -427,8 +432,8 @@ public void findByQuery() { // 4 Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowa")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).inCollection(collectionName).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()).block(); @@ -479,10 +484,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowe")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -526,13 +530,12 @@ public void existsByIdOther() { // 1 ExistsOptions existsOptions = ExistsOptions.existsOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowg")).block(); - try { Boolean exists = template.existsById().inScope(otherScope).inCollection(otherCollection) - .withOptions(existsOptions).one(vie.getId()).block(); - assertTrue(exists, "Airport should exist: " + vie.getId()); + .withOptions(existsOptions).one(saved.getId()).block(); + assertTrue(exists, "Airport should exist: " + saved.getId()); } finally { - template.removeById().inScope(otherScope).inCollection(otherCollection).one(vie.getId()).block(); + template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); } } @@ -571,8 +574,8 @@ public void findByQueryOther() { // 4 Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowj")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); @@ -623,10 +626,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lown")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -689,9 +691,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all().collectList().block()); + assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block()); } @Test @@ -729,9 +730,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> template.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block()); + () -> template.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all().collectList().block()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java index 6b4c7aa95..43ca51c36 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java @@ -39,6 +39,10 @@ public UUID getId() { return id; } + public String id(){ + return id.toString(); + } + /** * set the id */ diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index 8046e8a8b..74a88d09b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -46,6 +46,7 @@ public class Airport extends ComparableEntity { @Expiration private long expiration; @Max(2) long size; + private long someNumber; @PersistenceConstructor public Airport(String key, String iata, String icao) { 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 435c82bfc..f9f520f2c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -64,6 +64,7 @@ public interface AirportRepository extends CouchbaseRepository, List findByIataInAndIcaoIn(java.util.Collection size, java.util.Collection color, Pageable pageable); + // override an annotate with REQUEST_PLUS @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAll(); @@ -90,6 +91,12 @@ List findByIataInAndIcaoIn(java.util.Collection size, java.util @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Airport findByIataIn(JsonArray iatas); + @Query("Select \"\" AS __id, 0 AS __cas, substr(iata,0,1) as iata, count(*) as someNumber FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} GROUP BY substr(iata,0,1)") + List groupByIata(); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Airport findArchivedByIata(Iata iata); + // NOT_BOUNDED to test ScanConsistency // @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED) Airport iata(String iata); diff --git a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java new file mode 100644 index 000000000..ae3588c9b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java @@ -0,0 +1,8 @@ +package org.springframework.data.couchbase.domain; + +public class CollectionsConfig extends Config { + @Override + public String getScopeName(){ + return "my_scope"; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 27d50fc6f..e41ddc264 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -17,8 +17,6 @@ package org.springframework.data.couchbase.domain; import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Map; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @@ -62,7 +60,7 @@ @EnableReactiveCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") @EnableCaching public class Config extends AbstractCouchbaseConfiguration { - String bucketname = "travel-sample"; + String bucketname = "test"; String username = "Administrator"; String password = "password"; String connectionString = "127.0.0.1"; @@ -241,15 +239,14 @@ public String typeKey() { return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter } - static String scopeName = null; - @Override protected String getScopeName() { return scopeName; } - public static void setScopeName(String scopeName) { + static public void setScopeName(String scopeName) { Config.scopeName = scopeName; } + static private String scopeName = null; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java index 510aabae5..a1def5a59 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2021 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. @@ -13,11 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase.domain; -import static org.junit.jupiter.api.Assertions.assertEquals; - +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryProfile; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.query.QueryScanConsistency; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.util.Pair; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ParallelFlux; @@ -63,6 +76,8 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.query.QueryScanConsistency; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * @author Michael Reiche */ @@ -70,8 +85,13 @@ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class FluxIntegrationTests extends JavaIntegrationTests { - @BeforeAll - public static void beforeEverything() { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + + @BeforeEach + @Override + public void beforeEach() { + /** * The couchbaseTemplate inherited from JavaIntegrationTests uses org.springframework.data.couchbase.domain.Config * It has typeName = 't' (instead of _class). Don't use it. @@ -85,23 +105,19 @@ public static void beforeEverything() { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().upsert(k, JsonObject.create().put("x", k)); } + super.beforeEach(); } - @AfterAll - public static void afterEverthing() { + @AfterEach + public void afterEach() { couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + super.afterEach(); for (String k : keyList) { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().remove(k); } } - @BeforeEach - @Override - public void beforeEach() { - super.beforeEach(); - } - static List keyList = Arrays.asList("a", "b", "c", "d", "e"); static Collection collection; static ReactiveCollection rCollection; diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index eb2a4dd75..34c5a0184 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -22,13 +22,16 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.domain.Persistable; import org.springframework.lang.Nullable; @Document -public class Person extends AbstractEntity { +public class Person extends AbstractEntity implements Persistable { Optional firstname; @Nullable Optional lastname; @@ -47,13 +50,19 @@ public class Person extends AbstractEntity { private Address address; - public Person() {} + @Transient private boolean isNew; + + + public Person() { + setId( UUID.randomUUID()); + } public Person(String firstname, String lastname) { this(); setFirstname(firstname); setLastname(lastname); setMiddlename("Nick"); + isNew(true); } public Person(int id, String firstname, String lastname) { @@ -61,19 +70,24 @@ public Person(int id, String firstname, String lastname) { setId(new UUID(id, id)); } + public Person(UUID id, String firstname, String lastname) { + this(firstname, lastname); + setId(id); + } + static String optional(String name, Optional obj) { if (obj != null) { if (obj.isPresent()) { - return (" " + name + ": '" + obj.get() + "'\n"); + return (" " + name + ": '" + obj.get() + "'"); } else { - return " " + name + ": null\n"; + return " " + name + ": null"; } } return ""; } - public Optional getFirstname() { - return firstname; + public String getFirstname() { + return firstname.get(); } public void setFirstname(String firstname) { @@ -84,8 +98,8 @@ public void setFirstname(Optional firstname) { this.firstname = firstname; } - public Optional getLastname() { - return lastname; + public String getLastname() { + return lastname.get(); } public void setLastname(String lastname) { @@ -131,7 +145,7 @@ public String toString() { sb.append(optional(", firstname", firstname)); sb.append(optional(", lastname", lastname)); if (middlename != null) - sb.append(", middlename : " + middlename); + sb.append(", middlename : '" + middlename + "'"); sb.append(", version : " + version); if (creator != null) { sb.append(", creator : " + creator); @@ -148,8 +162,48 @@ public String toString() { if (getAddress() != null) { sb.append(", address : " + getAddress().toString()); } - sb.append("}"); + sb.append("\n}"); return sb.toString(); } + public Person withFirstName(String firstName) { + Person p = new Person(this.getId(), firstName, this.getLastname()); + p.version = version; + return p; + } + + // A with-er that returns the same object ?? + public Person withVersion(Long version) { + //Person p = new Person(this.getId(), this.getFirstname(), this.getLastname()); + this.version = version; + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + + Person that = (Person) obj; + return this.getId().equals(that.getId()) && this.getFirstname().equals(that.getFirstname()) + && this.getLastname().equals(that.getLastname()) && this.getMiddlename().equals(that.getMiddlename()); + } + + @Override + public boolean isNew() { + return isNew; + } + + public void isNew(boolean isNew){ + this.isNew = isNew; + } + + public Person withIdFirstname() { + return this.withFirstName(getId().toString()); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index 2c9985d74..b6f23ebe2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -18,9 +18,10 @@ import java.util.List; import java.util.UUID; +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import com.couchbase.client.java.query.QueryScanConsistency; @@ -28,7 +29,7 @@ /** * @author Michael Reiche */ -public interface PersonRepository extends CrudRepository { +public interface PersonRepository extends CouchbaseRepository, DynamicProxyable { /* * These methods are exercised in HomeController of the test spring-boot DemoApplication @@ -95,7 +96,7 @@ public interface PersonRepository extends CrudRepository { boolean existsById(UUID var1); - Iterable findAll(); + List findAll(); long count(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java new file mode 100644 index 000000000..ff4c60971 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 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.domain; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.lang.Nullable; + +@Document +public class PersonWithoutVersion extends AbstractEntity +{ + Optional firstname; + Optional lastname; + + public PersonWithoutVersion() { + firstname = Optional.empty(); + lastname = Optional.empty(); + } + + public PersonWithoutVersion(String firstname, String lastname) { + this.firstname = Optional.of(firstname); + this.lastname = Optional.of(lastname); + setId(UUID.randomUUID()); + } + + public PersonWithoutVersion(UUID id, String firstname, String lastname) { + this.firstname = Optional.of(firstname); + this.lastname = Optional.of(lastname); + setId(id); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 005b3f51e..a2aaaa7b4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.domain; +import lombok.val; +import org.springframework.data.couchbase.core.query.WithConsistency; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -46,6 +48,11 @@ public interface ReactiveAirportRepository extends ReactiveCouchbaseRepository, DynamicProxyable { + + @Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}") + @ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS) + Flux findIdByDynamicN1ql(String docType, String queryStatement); + @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Flux findAll(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java new file mode 100644 index 000000000..e28666513 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 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.domain; + +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; + +/** + * @author Michael Reiche + */ +public interface ReactivePersonRepository + extends ReactiveCouchbaseRepository, DynamicProxyable { + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 99a11fb5e..20fe84ee1 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -76,4 +76,7 @@ default List getByFirstname(String firstname) { } catch (InterruptedException ie) {} return findByFirstname(firstname); } + + @Override + User save(User user); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 87ec74ae5..4d46078dd 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -30,6 +30,9 @@ import java.util.Optional; import java.util.UUID; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.java.env.ClusterEnvironment; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; 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 fbb945824..39daea436 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -18,6 +18,8 @@ import static com.couchbase.client.java.query.QueryScanConsistency.NOT_BOUNDED; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static com.couchbase.client.java.query.QueryOptions.queryOptions; +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; @@ -109,6 +111,8 @@ import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -141,6 +145,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr public void beforeEach() { super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all(); @@ -283,8 +288,7 @@ void findBySimpleProperty() { try { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); - Airport airport2 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + Airport airport2 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS)) .findByIata(vie.getIata()); assertEquals(airport2, vie); @@ -388,7 +392,7 @@ public void saveRequestPlusWithDefaultRepository() { Airport vie = new Airport("airports::vie", "vie", "low9"); Airport saved = airportRepositoryRP.save(vie); - List allSaved = airportRepositoryRP.findAll(); + List allSaved = airportRepositoryRP.findAll(REQUEST_PLUS); couchbaseTemplate.removeById(Airport.class).one(saved.getId()); assertEquals(1, allSaved.size(), "should have found 1 airport"); } @@ -400,9 +404,10 @@ void findByTypeAlias() { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); List airports = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS) .matching(org.springframework.data.couchbase.core.query.Query .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) + .withConsistency(REQUEST_PLUS) + .all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { @@ -462,15 +467,13 @@ void findBySimplePropertyWithCollection() { Airport saved = airportRepository.withScope(scopeName).withCollection(collectionName).save(vie); // given collection (on scope used by template) Airport airport2 = airportRepository.withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport2); // given scope and collection Airport airport3 = airportRepository.withScope(scopeName).withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport3); // given bad collection @@ -478,7 +481,8 @@ void findBySimplePropertyWithCollection() { () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); // given bad scope - assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + assertThrows(IndexFailureException.class, + () -> airportRepository.withScope("bogusScope").withCollection(collectionName).iata(vie.getIata())); } finally { airportRepository.delete(vie); @@ -512,12 +516,11 @@ void findBySimplePropertyWithOptions() { try { Airport saved = airportRepository.save(vie); // Duration of 1 nano-second will cause timeout - assertThrows(AmbiguousTimeoutException.class, () -> airportRepository - .withOptions(QueryOptions.queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); + assertThrows(AmbiguousTimeoutException.class, + () -> airportRepository.withOptions(queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); - Airport airport3 = airportRepository.withOptions( - QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) - .iata(vie.getIata()); + Airport airport3 = airportRepository + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS).parameters(positionalParams)).iata(vie.getIata()); assertEquals(saved, airport3); } finally { @@ -535,7 +538,8 @@ public void saveNotBounded() { // set version == 0 so save() will be an upsert, not a replace Airport saved = airportRepository.save(vie.clearVersion()); try { - airport2 = airportRepository.iata(saved.getIata()); + airport2 = airportRepository.withOptions(queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .iata(saved.getIata()); if (airport2 == null) { break; } @@ -548,7 +552,8 @@ public void saveNotBounded() { assertEquals(vie.getId(), removeResult.getId()); assertTrue(removeResult.getCas() != 0); assertTrue(removeResult.getMutationToken().isPresent()); - Airport airport3 = airportRepository.iata(vie.getIata()); + Airport airport3 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS) + .consistentWith(MutationState.from(removeResult.getMutationToken().get()))).iata(vie.getIata()); assertNull(airport3, "should have been removed"); } } @@ -717,6 +722,24 @@ void countSlicePage() { } } + @Test + void testGroupBy() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + try { + airportRepository.saveAll( + Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet())); + List airports = airportRepository.groupByIata(); + for (Airport a : airports) { + System.out.println(a); + } + + } finally { + airportRepository + .deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + @Test void badCount() { assertThrows(CouchbaseQueryExecutionException.class, () -> airportRepository.countBad()); @@ -868,11 +891,11 @@ void threadSafeStringParametersTest() throws Exception { } @Test - // DATACOUCH-650 + // DATACOUCH-650 void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDZ"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { airportRepository.saveAll(asList(vienna, frankfurt, losAngeles)); @@ -887,8 +910,8 @@ void deleteAllById() { void couchbaseRepositoryQuery() throws Exception { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); - couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")).all(); + couchbaseTemplate.findByQuery(User.class).matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")) + .withConsistency(REQUEST_PLUS).all(); String input = "findByFirstname"; Method method = UserRepository.class.getMethod(input, String.class); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 4c0e1819c..f3161ff68 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -96,6 +96,28 @@ void shouldSaveAndFindAll() { } } + @Test + void testQuery() { + Airport vie = null; + Airport jfk = null; + try { + vie = new Airport("airports::vie", "vie", "low1"); + airportRepository.save(vie).block(); + jfk = new Airport("airports::jfk", "JFK", "xxxx"); + airportRepository.save(jfk).block(); + + List all = airportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); + System.out.println(all); + assertFalse(all.isEmpty()); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::vie"))); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::jfk"))); + + } finally { + airportRepository.delete(vie).block(); + airportRepository.delete(jfk).block(); + } + } + @Test void findBySimpleProperty() { Airport vie = null; @@ -219,7 +241,7 @@ void count() { void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDX"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { @@ -242,7 +264,7 @@ void deleteAllById() { void deleteAll() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDY"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index cf1a63159..e7d962346 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -31,11 +31,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.AddressAnnotated; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; import org.springframework.data.couchbase.domain.AirportRepositoryAnnotated; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserCol; @@ -62,13 +66,17 @@ */ @SpringJUnitConfig(Config.class) @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { - @Autowired AirportRepository airportRepository; // initialized in beforeEach() - @Autowired UserColRepository userColRepository; // initialized in beforeEach() - @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; // initialized in beforeEach() - @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; // initialized in beforeEach() @Autowired AirportRepositoryAnnotated airportRepositoryAnnotated; + @Autowired AirportRepository airportRepository; + @Autowired UserColRepository userColRepository; + @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; + @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; + + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { @@ -222,9 +230,14 @@ public void testScopeCollectionAnnotationSwap() { // collection from CrudMethodMetadata of UserCol.save() UserCol userCol = new UserCol("1", "Dave", "Wilson"); Airport airport = new Airport("3", "myIata", "myIcao"); - UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + try { + UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + } finally { + List removed = couchbaseTemplate.removeByQuery(Airport.class).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } } // template default scope is my_scope @@ -278,7 +291,8 @@ void findPlusN1qlJoinBothAnnotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionAnnotatedRepository.findByUsername(user.getUsername()); @@ -332,7 +346,8 @@ void findPlusN1qlJoinUnannotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionUnannotatedRepository.findByUsername(user.getUsername()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index be2a14861..3aac657f8 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -27,7 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveAirportRepositoryAnnotated; @@ -38,6 +41,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.core.io.CollectionIdentifier; @@ -53,11 +57,14 @@ */ @SpringJUnitConfig(Config.class) @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired ReactiveAirportRepository reactiveAirportRepository; @Autowired ReactiveAirportRepositoryAnnotated reactiveAirportRepositoryAnnotated; @Autowired ReactiveUserColRepository userColRepository; + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index cebcdc1d4..cfd4ebb89 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -43,6 +43,8 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * @author Michael Nitschinger * @author Michael Reiche @@ -69,11 +71,31 @@ void wrongNumberArgs() throws Exception { converter.getMappingContext()); try { - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, - namedQueries); - } catch (IllegalArgumentException e) { - return; + Airline modified = couchbaseTemplate.upsertById(Airline.class).one(airline); + + String input = "getByName"; + Method method = AirlineRepository.class.getMethod(input, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(AirlineRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), + queryMethod, converter, config().bucketname(), new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + + ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class).matching(query) + .withConsistency(QueryScanConsistency.REQUEST_PLUS); + + Optional al = q.one(); + assertEquals(airline.toString(), al.get().toString()); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + couchbaseTemplate.removeById().one(airline.getId()); } fail("should have failed with IllegalArgumentException: Invalid number of parameters given!"); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java new file mode 100644 index 000000000..1771e909e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java @@ -0,0 +1,33 @@ +package org.springframework.data.couchbase.transactions; + +import lombok.Data; + +import org.springframework.data.domain.Persistable; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@Data +public class AfterTransactionAssertion { + + private final T persistable; + private boolean expectToBePresent; + + public void isPresent() { + expectToBePresent = true; + } + + public void isNotPresent() { + expectToBePresent = false; + } + + public Object getId() { + return persistable.getId(); + } + + public boolean shouldBePresent() { + return expectToBePresent; + } +} + diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java new file mode 100644 index 000000000..00e05002b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -0,0 +1,350 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 com.couchbase.client.core.error.DocumentExistsException; +import lombok.Data; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Disabled; +import org.springframework.data.couchbase.core.TransactionalSupport; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.reactive.TransactionalOperator; + +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for com.couchbase.transactions using + *
  • couchbase reactive transaction manager via transactional operator couchbase non-reactive transaction + * manager via @Transactional @Transactional(transactionManager = + * BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)
  • + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, PersonService.class }) +public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonRepository repo; + @Autowired ReactivePersonRepository rxRepo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired PersonService personService; + @Autowired TransactionalOperator transactionalOperator; + + String sName = "_default"; + String cName = "_default"; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @AfterEach + public void afterEachTest() { + TransactionTestUtil.assertNotInTransaction(); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = cbTmpl.removeByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List rp2 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List rp3 = cbTmpl.removeByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p1 = cbTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).withConsistency(REQUEST_PLUS) + .all(); + List e0 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List e1 = cbTmpl.findByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + } + + @DisplayName("rollback after exception using transactionalOperator") + @Test + public void shouldRollbackAfterException() { + assertThrowsWithCause(() -> personService.savePersonErrors(WalterWhite), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + @DisplayName("rollback after exception using @Transactional") + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + @DisplayName("rollback after exception after using @Transactional(reactive)") + public void shouldRollbackAfterExceptionOfTxAnnotatedMethodReactive() { + assertThrowsWithCause(() -> personService.declarativeSavePersonErrorsReactive(WalterWhite).block(), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = personService.savePerson(WalterWhite); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + Person p = personService.declarativeSavePerson(WalterWhite); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + /** + * This fails with TransactionOperationFailedException {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't + * know why it isn't retried. This seems like it is due to the functioning of AbstractPlatformTransactionManager + */ + public void replaceInTxAnnotatedCallback() { + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethodReactive() { + Person p = personService.declarativeSavePersonReactive(WalterWhite).block(); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesAcrossCollections() { + List persons = personService.saveWithLogs(WalterWhite); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + Long countEvents = cbTmpl.count(new Query(), EventLog.class); // + assertEquals(4, countEvents, "should have saved and found 4"); + } + + @Test + public void rollbackShouldAbortAcrossCollections() { + assertThrowsWithCause(() -> personService.saveWithErrorLogs(WalterWhite), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + List persons = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + assertEquals(0, persons.size(), "should have done roll back and left 0 entries"); + List events = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); // + assertEquals(0, events.size(), "should have done roll back and left 0 entries"); + } + + @Test + public void countShouldWorkInsideTransaction() { + Long count = personService.countDuringTx(WalterWhite); + assertEquals(1, count, "should have counted 1 during tx"); + } + + @Test + public void emitMultipleElementsDuringTransaction() { + List docs = personService.saveWithLogs(WalterWhite); + assertEquals(4, docs.size(), "should have found 4 eventlogs"); + } + + @Test + public void errorAfterTxShouldNotAffectPreviousStep() { + Person p = personService.savePerson(WalterWhite); + assertThrowsOneOf(() -> personService.savePerson(p), TransactionSystemUnambiguousException.class, + DocumentExistsException.class); + Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void replacePersonCBTransactionsRxTmpl() { + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Mono result = rxCBTmpl.findById(Person.class).one(person.id()) // + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp)).doOnNext(ppp -> TransactionalSupport + .checkForTransactionInThreadLocalStorage().doOnNext(v -> assertTrue(v.isPresent()))) + .as(transactionalOperator::transactional); + result.block(); + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + assertEquals(person, pFound, "should have found expected " + person); + } + + @Test + public void insertPersonCBTransactionsRxTmplRollback() { + Mono result = rxCBTmpl.insertById(Person.class).one(WalterWhite) // + .doOnNext(ppp -> TransactionalSupport.checkForTransactionInThreadLocalStorage() + .doOnNext(v -> assertTrue(v.isPresent()))) + .map(p -> throwSimulateFailureException(p)).as(transactionalOperator::transactional); // tx + assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).one(WalterWhite.id()); + assertNull(pFound, "insert should have been rolled back"); + } + + @Test + public void insertTwicePersonCBTransactionsRxTmplRollback() { + Mono result = rxCBTmpl.insertById(Person.class).one(WalterWhite) // + .flatMap(ppp -> rxCBTmpl.insertById(Person.class).one(ppp)) // + .as(transactionalOperator::transactional); + assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, DuplicateKeyException.class); + Person pFound = cbTmpl.findById(Person.class).one(WalterWhite.id()); + assertNull(pFound, "insert should have been rolled back"); + } + + /** + * I think this test might fail sometimes? Does it need retryWhen() ? + */ + @Disabled("todo gp: disabling temporarily as hanging intermittently") + @Test + public void wrapperReplaceWithCasConflictResolvedViaRetry() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + String newName = "Dave"; + + TransactionResult txResult = couchbaseClientFactory.getCluster().transactions().run(ctx -> { + Person ppp = cbTmpl.findById(Person.class).one(person.id()); + ReplaceLoopThread.updateOutOfTransaction(cbTmpl, person, tryCount.incrementAndGet()); + Person pppp = cbTmpl.replaceById(Person.class).one(ppp.withFirstName(newName)); + }); + + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(newName, pFound.getFirstname(), "should have been switched"); + } + + /** + * This does process retries - by CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() + * -> retryWhen. + */ + /** + * This fails with TransactionOperationFailedException {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't + * know why it isn't retried. This seems like it is due to the functioning of AbstractPlatformTransactionManager + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(); + + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + /** + * Reactive @Transactional does not retry write-write conflicts. It throws RetryTransactionException up to the client + * and expects the client to retry. + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(); + + Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) + .block(); + + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotated() { + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Person switchedPerson = person.withFirstName("Dave"); + AtomicInteger tryCount = new AtomicInteger(); + Person p = personService.declarativeFindReplacePerson(switchedPerson, tryCount); + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + System.out.println("pFound: " + pFound); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + @Data + static class EventLog { + + public EventLog() {}; // don't remove this + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + String id; + String action; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("EventLog : {\n"); + sb.append(" id : " + getId()); + sb.append(", action: " + action); + return sb.toString(); + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java new file mode 100644 index 000000000..dbed77627 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -0,0 +1,242 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import lombok.Data; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.LinkedList; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + *

    + * todo gp: these tests are using the `.as(transactionalOperator::transactional)` method which is for the chopping block, so presumably these tests are too + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, PersonServiceReactive.class }) +public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired Cluster myCluster; + @Autowired PersonServiceReactive personService; + @Autowired ReactiveCouchbaseTemplate operations; + + // if these are changed from default, then beforeEach needs to clean up separately + String sName = "_default"; + String cName = "_default"; + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + List pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); + List er = operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + List e = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + } + + @Test + public void shouldRollbackAfterException() { + personService.savePersonErrors(WalterWhite) // + .as(StepVerifier::create) // + .verifyError(TransactionSystemUnambiguousException.class); + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).blockLast(), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + } + + @Test + public void commitShouldPersistTxEntries() { + + personService.savePerson(WalterWhite) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test + /* todo - does this need to be in + Caused by: java.lang.UnsupportedOperationException: Return type is Mono or Flux, indicating a reactive transaction + is being performed in a blocking way. A potential cause is the CouchbaseSimpleTransactionInterceptor is not in use. + */ + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + + personService.declarativeSavePerson(WalterWhite).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + } + + @Test + public void commitShouldPersistTxEntriesAcrossCollections() { + + personService.saveWithLogs(WalterWhite) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(4L) // + .verifyComplete(); + } + + @Test + public void rollbackShouldAbortAcrossCollections() { + + personService.saveWithErrorLogs(WalterWhite) // + .then() // + .as(StepVerifier::create) // + .verifyError(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).count()// + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test + public void countShouldWorkInsideTransaction() { + personService.countDuringTx(WalterWhite) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test + public void emitMultipleElementsDuringTransaction() { + personService.saveWithLogs(WalterWhite) // + .as(StepVerifier::create) // + .expectNextCount(4L) // + .verifyComplete(); + } + + @Test + public void errorAfterTxShouldNotAffectPreviousStep() { + + personService.savePerson(WalterWhite) // + .then(Mono.error(new SimulateFailureException())).as(StepVerifier::create) // + .verifyError(); + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + @Version Long version; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java new file mode 100644 index 000000000..8610c9a15 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -0,0 +1,228 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * Tests for CouchbaseTransactionalOperator. + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class CouchbaseReactiveTransactionNativeTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired ReactiveCouchbaseTemplate operations; + // This will pick up CouchbaseTransactionalOperator + @Autowired TransactionalOperator txOperator; + + String sName = "_default"; + String cName = "_default"; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + assertTrue(txOperator instanceof CouchbaseTransactionalOperator); + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + TransactionTestUtil.assertNotInTransaction(); + List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = cbTmpl.removeByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p1 = cbTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).withConsistency(REQUEST_PLUS) + .all(); + } + + @Test + public void replacePersonTemplate() { + Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); + Flux result = txOperator.execute((ctx) -> rxCBTmpl.findById(Person.class).one(person.id()) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))); + result.blockLast(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void replacePersonRbTemplate() { + Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); + Flux result = txOperator.execute((ctx) -> rxCBTmpl.findById(Person.class).one(person.id()) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) + .map(it -> throwSimulateFailureException(it))); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person, pFound, "Should have found " + person); + } + + @Test + public void insertPersonTemplate() { + Person person = WalterWhite; + Flux result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).one(person) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))); + result.blockLast(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = WalterWhite; + Flux result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).one(person) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) + .map(it -> throwSimulateFailureException(it))); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void replacePersonRbRepo() { + Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); + Flux result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).findById(person.id()) + .flatMap(p -> rxRepo.withCollection(cName).save(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException()))); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = rxRepo.withCollection(cName).findById(person.id()).block(); + assertEquals(person, pFound, "Should have found " + person); + } + + @Test + public void insertPersonRbRepo() { + Person person = WalterWhite; + Flux result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).save(person) // insert + .map(it -> throwSimulateFailureException(it))); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = rxRepo.withCollection(cName).findById(person.id()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void insertPersonRepo() { + Person person = WalterWhite; + Flux result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).save(person) // insert + .flatMap(p -> rxRepo.withCollection(cName).save(p.withFirstName("Walt")))); + result.blockLast(); + Person pFound = rxRepo.withCollection(cName).findById(person.id()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void replacePersonSpringTransactional() { + Person person = WalterWhite; + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + Mono result = rxCBTmpl.findById(Person.class).one(person.id()) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))).as(txOperator::transactional); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found " + person); + } + + @Test + public void replacePersonRbSpringTransactional() { + Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); + Mono result = rxCBTmpl.findById(Person.class).one(person.id()) + .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).as(txOperator::transactional); + assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person, pFound, "Should have found " + person); + assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname()); + } + + @Test + public void findReplacePersonCBTransactionsRxTmpl() { + Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block(); + Flux result = txOperator.execute(ctx -> rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()) + .flatMap(pGet -> rxCBTmpl.replaceById(Person.class).inCollection(cName).one(pGet.withFirstName("Walt")))); + result.blockLast(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test + public void insertReplacePersonsCBTransactionsRxTmpl() { + Person person = WalterWhite; + Flux result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).inCollection(cName).one(person) + .flatMap(pInsert -> rxCBTmpl.replaceById(Person.class).inCollection(cName).one(pInsert.withFirstName("Walt")))); + result.blockLast(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test + void transactionalSavePerson() { + Person person = WalterWhite; + savePerson(person).block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); + assertEquals(person, pFound, "Should have found " + person); + } + + public Mono savePerson(Person person) { + return operations.save(person) // + .as(txOperator::transactional); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java new file mode 100644 index 000000000..c06014dc1 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.transaction.TransactionManager; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.reactive.TransactionalOperator; + +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + *

    + * Tests CouchbaseTransactionalOperator. + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +// I think these are all redundant (see CouchbaseReactiveTransactionNativeTests). There does not seem to be a blocking +// form of TransactionalOperator. Also there does not seem to be a need for a CouchbaseTransactionalOperator as +// TransactionalOperator.create(reactiveCouchbaseTransactionManager) seems to work just fine. (I don't recall what +// merits the "Native" in the name). +public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired TransactionManager couchbaseTransactionManager; + @Autowired PersonRepository repo; + @Autowired ReactivePersonRepository repoRx; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCbTmpl; + @Autowired TransactionalOperator txOperator; + static String cName; // short name + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + // short names + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEach() { + assertTrue(txOperator instanceof CouchbaseTransactionalOperator); + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + } + + @AfterEach + public void afterEach() { + TransactionTestUtil.assertNotInTransaction(); + } + + @Test + public void replacePersonTemplate() { + Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); + assertThrowsWithCause(() -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.id()) // + .flatMap(pp -> rxCbTmpl.replaceById(Person.class).one(pp.withIdFirstname()) // + .map(ppp -> throwSimulateFailureException(ppp)))) + .blockLast(), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname()); + + } + + @Test + public void replacePersonRbTemplate() { + Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); + assertThrowsWithCause( + () -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withIdFirstname())) // + .map(ppp -> throwSimulateFailureException(ppp))).blockLast(), // + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname()); + + } + + @Test + public void insertPersonTemplate() { + txOperator.execute((ctx) -> rxCbTmpl.insertById(Person.class).one(WalterWhite) + .flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))).blockLast(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void insertPersonRbTemplate() { + assertThrowsWithCause( + () -> txOperator.execute((ctx) -> rxCbTmpl.insertById(Person.class).one(WalterWhite) + .flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) + .map(it -> throwSimulateFailureException(it))).blockLast(), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void replacePersonRbRepo() { + Person person = repo.withCollection(cName).save(WalterWhite); + assertThrowsWithCause(() -> txOperator.execute(ctx -> { + return repoRx.withCollection(cName).findById(person.id()) + .flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt"))) + .map(pp -> throwSimulateFailureException(pp)); + }).blockLast(), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertEquals(person, pFound, "Should have found " + person); + } + + @Test + public void insertPersonRbRepo() { + assertThrowsWithCause(() -> txOperator.execute((ctx) -> repoRx.withCollection(cName).save(WalterWhite) // insert + .flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt"))) // replace + .map(it -> throwSimulateFailureException(it))).blockLast(), TransactionSystemUnambiguousException.class, + SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void insertPersonRepo() { + txOperator.execute((ctx) -> repoRx.withCollection(cName).save(WalterWhite) // insert + .flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt"))) // replace + ).blockFirst(); + Optional pFound = repo.withCollection(cName).findById(WalterWhite.id()); + assertEquals("Walt", pFound.get().getFirstname(), "firstname should be Walt"); + } + + @Test + public void replacePersonRbSpringTransactional() { + Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); + assertThrowsWithCause( + () -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.getId().toString()) + .flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) + .map(it -> throwSimulateFailureException(it))).blockLast(), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be Walter"); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java new file mode 100644 index 000000000..49345ee81 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2022 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.transactions; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, where operations that aren't supported in a transaction are being used. + * They should be prevented at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = {TransactionsConfig.class, CouchbaseTransactionalNonAllowableOperationsIntegrationTests.PersonService.class}) +public class CouchbaseTransactionalNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + } + + void test(Consumer r) { + AtomicInteger tryCount = new AtomicInteger(0); + +assertThrowsWithCause( () -> { + personService.doInTransaction(tryCount, (ops) -> { + r.accept(ops); + return null; + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using existsById() in a transaction is rejected at runtime") + @Test + public void existsById() { + test((ops) -> { + ops.existsById(Person.class).one(WalterWhite.id()); + }); + } + + @DisplayName("Using findByAnalytics() in a transaction is rejected at runtime") + @Test + public void findByAnalytics() { + test((ops) -> { + ops.findByAnalytics(Person.class).one(); + }); + } + + @DisplayName("Using findFromReplicasById() in a transaction is rejected at runtime") + @Test + public void findFromReplicasById() { + test((ops) -> { + ops.findFromReplicasById(Person.class).any(WalterWhite.id()); + }); + } + + @DisplayName("Using upsertById() in a transaction is rejected at runtime") + @Test + public void upsertById() { + test((ops) -> { + ops.upsertById(Person.class).one(WalterWhite); + }); + } + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + final CouchbaseOperations personOperations; + + public PersonService(CouchbaseOperations ops) { + personOperations = ops; + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java new file mode 100644 index 000000000..ae10994ba --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.reactive.TransactionalOperator; + +import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for CouchbaseTransactionalOperator, using template methods (findById etc.) + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class CouchbaseTransactionalOperatorTemplateIntegrationTests extends JavaIntegrationTests { + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTemplate ops; + @Autowired CouchbaseTemplate blocking; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + assertNotInTransaction(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + static class RunResult { + public final int attempts; + + public RunResult(int attempts) { + this.attempts = attempts; + } + } + + private RunResult doMonoInTransaction(Supplier> lambda) { + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( + couchbaseClientFactory); + TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); + AtomicInteger attempts = new AtomicInteger(); + + operator.transactional(Mono.fromRunnable(() -> attempts.incrementAndGet()).then(lambda.get())).block(); + + assertNotInTransaction(); + + return new RunResult(attempts.get()); + } + + @DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.execute should succeed") + @Test + public void committedInsertWithExecute() { + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( + couchbaseClientFactory); + TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); + + operator.execute(v -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite); + }); + }).blockLast(); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + } + + @DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.transactional(Flux) should succeed") + @Test + public void committedInsertWithFlux() { + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( + couchbaseClientFactory); + TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); + + Flux flux = Flux.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite); + }); + + operator.transactional(flux).blockLast(); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + } + + @DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.transactional(Mono) should succeed") + @Test + public void committedInsert() { + + RunResult rr = doMonoInTransaction(() -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite); + }); + }); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + Person p = blocking.insertById(Person.class).one(WalterWhite); + + RunResult rr = doMonoInTransaction(() -> { + return ops.findById(Person.class).one(WalterWhite.id()).flatMap(person -> { + person.setFirstname("changed"); + return ops.replaceById(Person.class).one(person); + }); + }); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + + Person person = blocking.insertById(Person.class).one(WalterWhite); + + RunResult rr = doMonoInTransaction(() -> { + return ops.findById(Person.class).one(person.id()) + .flatMap(fetched -> ops.removeById(Person.class).oneEntity(fetched)); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doMonoInTransaction(() -> { + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.id())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().next(); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path findByQuery should succeed") + @Test + public void committedFindByQuery() { + Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doMonoInTransaction(() -> { + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.id())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().next(); + }); + + assertEquals(1, rr.attempts); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger attempts = new AtomicInteger(); + + assertThrowsWithCause(() -> doMonoInTransaction(() -> { + attempts.incrementAndGet(); + return ops.insertById(Person.class).one(WalterWhite).map((p) -> throwSimulateFailureException(p)); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.toString()); + assertNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doMonoInTransaction(() -> { + attempts.incrementAndGet(); + return ops.findById(Person.class).one(person.id()) // + .flatMap(p -> ops.replaceById(Person.class).one(p.withFirstName("changed"))) // + .map(p -> throwSimulateFailureException(p)); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertEquals(person.getFirstname(), fetched.getFirstname()); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doMonoInTransaction(() -> { + attempts.incrementAndGet(); + return ops.findById(Person.class).one(person.id()) + .flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // + .doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doMonoInTransaction(() -> { + attempts.incrementAndGet(); + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all() + .elementAt(0).map(p -> throwSimulateFailureException(p)); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doMonoInTransaction(() -> { + attempts.incrementAndGet(); + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all() + .elementAt(0).map(p -> throwSimulateFailureException(p)); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + assertEquals(1, attempts.get()); + } + + @DisplayName("Forcing CAS mismatch causes a transaction retry") + @Test + public void casMismatchCausesRetry() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + // Needs to take place in a separate thread to bypass the ThreadLocalStorage checks + Thread forceCASMismatch = new Thread(() -> { + Person fetched = blocking.findById(Person.class).one(person.id()); + blocking.replaceById(Person.class).one(fetched.withFirstName("Changed externally")); + }); + + doMonoInTransaction(() -> { + return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.defer(() -> { + + if (attempts.incrementAndGet() == 1) { + forceCASMismatch.start(); + try { + forceCASMismatch.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")); + })); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertEquals("Changed by transaction", fetched.getFirstname()); + assertEquals(2, attempts.get()); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java new file mode 100644 index 000000000..453d187c9 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2022 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.transactions; + +import com.couchbase.client.core.error.transaction.AttemptExpiredException; +import com.couchbase.client.java.transactions.error.TransactionExpiredException; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.TransactionTimedOutException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, setting all the various options allowed by @Transactional. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalOptionsIntegrationTests.PersonService.class }) +public class CouchbaseTransactionalOptionsIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + } + + @DisplayName("@Transactional(timeout = 2) will timeout at around 2 seconds") + @Test + public void timeout() { + long start = System.nanoTime(); + Person person = operations.insertById(Person.class).one(WalterWhite); + assertThrowsWithCause(() -> { + personService.timeout(person.id()); + }, TransactionSystemUnambiguousException.class, AttemptExpiredException.class); + Duration timeTaken = Duration.ofNanos(System.nanoTime() - start); + assertTrue(timeTaken.toMillis() >= 2000); + assertTrue(timeTaken.toMillis() < 10_000); // Default transaction timeout is 15s + } + + @DisplayName("@Transactional(isolation = Isolation.ANYTHING_BUT_READ_COMMITTED) will fail") + @Test + public void unsupportedIsolation() { + assertThrowsWithCause(() -> { + personService.unsupportedIsolation(); + }, IllegalArgumentException.class); + + } + + @DisplayName("@Transactional(isolation = Isolation.READ_COMMITTED) will succeed") + @Test + public void supportedIsolation() { + personService.supportedIsolation(); + } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + final CouchbaseOperations ops; + + public PersonService(CouchbaseOperations ops) { + this.ops = ops; + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(ops); + } + + @Transactional(timeout = 2) + public void timeout(String id) { + while (true) { + Person p = ops.findById(Person.class).one(id); + ops.replaceById(Person.class).one(p); + } + } + + @Transactional(isolation = Isolation.REPEATABLE_READ) + public void unsupportedIsolation() {} + + @Transactional(isolation = Isolation.READ_COMMITTED) + public void supportedIsolation() {} + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java new file mode 100644 index 000000000..96da1b42f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java @@ -0,0 +1,358 @@ +/* + * Copyright 2012-2022 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.transactions; + +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.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for the various propagation values allowed on @Transactional methods. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalPropagationIntegrationTests.PersonService.class }) +public class CouchbaseTransactionalPropagationIntegrationTests extends JavaIntegrationTests { + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionalPropagationIntegrationTests.class); + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + assertNotInTransaction(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("Call @Transactional(propagation = DEFAULT) - succeeds, creates a transaction") + @Test + public void callDefault() { + personService.propagationDefault(ops -> { + assertInTransaction(); + }); + } + + @DisplayName("Call @Transactional(propagation = SUPPORTS) - fails as unsupported") + @Test + public void callSupports() { + assertThrowsWithCause(() -> personService.propagationSupports(ops -> {}), UnsupportedOperationException.class); + } + + @DisplayName("Call @Transactional(propagation = MANDATORY) - fails as not in an active transaction") + @Test + public void callMandatory() { + assertThrowsWithCause(() -> personService.propagationMandatory(ops -> {}), IllegalTransactionStateException.class); + } + + @DisplayName("Call @Transactional(propagation = REQUIRES_NEW) - fails as unsupported") + @Test + public void callRequiresNew() { + assertThrowsWithCause(() -> personService.propagationRequiresNew(ops -> {}), UnsupportedOperationException.class); + + } + + @DisplayName("Call @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported") + @Test + public void callNotSupported() { + assertThrowsWithCause(() -> personService.propagationNotSupported(ops -> {}), UnsupportedOperationException.class); + } + + @DisplayName("Call @Transactional(propagation = NEVER) - succeeds as not in a transaction, starts one") + @Test + public void callNever() { + personService.propagationNever(ops -> { + assertInTransaction(); + }); + } + + @DisplayName("Call @Transactional(propagation = NESTED) - succeeds as not in an existing transaction, starts one") + @Test + public void callNested() { + personService.propagationNested(ops -> { + assertInTransaction(); + }); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = DEFAULT) - succeeds, continues existing") + @Test + public void callDefaultThatCallsDefault() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationDefault(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everything committed + + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRED) - succeeds, continues existing") + @Test + public void callDefaultThatCallsRequired() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationRequired(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everything committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = MANDATORY) - succeeds, continues existing") + @Test + public void callDefaultThatCallsMandatory() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationMandatory(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + + assertInTransaction(); + }); + }); + + // Validate everything committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRES_NEW) - fails as unsupported") + @Test + public void callDefaultThatCallsRequiresNew() { + UUID id1 = UUID.randomUUID(); + + assertThrowsWithCause(() -> personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + personService.propagationRequiresNew(ops2 -> {}); + }), TransactionSystemUnambiguousException.class, UnsupportedOperationException.class); + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported") + @Test + public void callDefaultThatCallsNotSupported() { + UUID id1 = UUID.randomUUID(); + + assertThrowsWithCause(() -> { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + personService.propagationNotSupported(ops2 -> {}); + }); + }, TransactionSystemUnambiguousException.class, UnsupportedOperationException.class); + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NEVER) - fails as in a transaction") + @Test + public void callDefaultThatCallsNever() { + UUID id1 = UUID.randomUUID(); + + assertThrowsWithCause(() -> { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + personService.propagationNever(ops2 -> {}); + }); + }, TransactionSystemUnambiguousException.class, IllegalTransactionStateException.class); + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = NESTED) - fails as unsupported") + @Test + public void callDefaultThatCallsNested() { + UUID id1 = UUID.randomUUID(); + + assertThrowsWithCause(() -> { + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationNested(ops2 -> {}); + }); + }, TransactionSystemUnambiguousException.class, UnsupportedOperationException.class); + + // Validate everything rolled back + assertNull(operations.findById(Person.class).one(id1.toString())); + } + + @DisplayName("Call @Transactional that calls @Transactional(propagation = DEFAULT) - check retries act correct") + @Test + public void callDefaultThatCallsDefaultRetries() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + AtomicInteger attempts = new AtomicInteger(); + + personService.propagationDefault(ops -> { + ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace")); + + personService.propagationDefault(ops2 -> { + ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper")); + assertInTransaction(); + + if (attempts.incrementAndGet() < 3) { + throw new RetryTransactionException(); + } + }); + }); + + // Validate everything committed + assertNotNull(operations.findById(Person.class).one(id1.toString())); + assertNotNull(operations.findById(Person.class).one(id2.toString())); + assertEquals(3, attempts.get()); + } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + final CouchbaseOperations ops; + + public PersonService(CouchbaseOperations ops) { + this.ops = ops; + } + + @Transactional + public void propagationDefault(@Nullable Consumer callback) { + LOGGER.info("propagationDefault"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.REQUIRED) + public void propagationRequired(@Nullable Consumer callback) { + LOGGER.info("propagationRequired"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.MANDATORY) + public void propagationMandatory(@Nullable Consumer callback) { + LOGGER.info("propagationMandatory"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.NESTED) + public void propagationNested(@Nullable Consumer callback) { + LOGGER.info("propagationNever"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public void propagationSupports(@Nullable Consumer callback) { + LOGGER.info("propagationSupports"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public void propagationNotSupported(@Nullable Consumer callback) { + LOGGER.info("propagationNotSupported"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void propagationRequiresNew(@Nullable Consumer callback) { + LOGGER.info("propagationRequiresNew"); + if (callback != null) + callback.accept(ops); + } + + @Transactional(propagation = Propagation.NEVER) + public void propagationNever(@Nullable Consumer callback) { + LOGGER.info("propagationNever"); + if (callback != null) + callback.accept(ops); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java new file mode 100644 index 000000000..15aad9714 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2022 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.transactions; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests @Transactional with repository methods. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalRepositoryIntegrationTests.UserService.class }) +public class CouchbaseTransactionalRepositoryIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired UserRepository userRepo; + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired UserService userService; + @Autowired CouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + assertNotInTransaction(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @Test + public void findByFirstname() { + operations.insertById(User.class).one(new User(UUID.randomUUID().toString(), "Ada", "Lovelace")); + + List users = userService.findByFirstname("Ada"); + + assertNotEquals(0, users.size()); + } + + @Test + public void save() { + String id = UUID.randomUUID().toString(); + + userService.run(repo -> { + assertInTransaction(); + + User user0 = repo.save(new User(id, "Ada", "Lovelace")); + + assertInTransaction(); + + // read your own write + User user1 = operations.findById(User.class).one(id); + assertNotNull(user1); + + assertInTransaction(); + + }); + + User user = operations.findById(User.class).one(id); + assertNotNull(user); + } + + @DisplayName("Test that repo.save() is actually performed transactionally, by forcing a rollback") + @Test + public void saveRolledBack() { + String id = UUID.randomUUID().toString(); + + assertThrowsWithCause( () -> {; + userService.run(repo -> { + User user = repo.save(new User(id, "Ada", "Lovelace")); + SimulateFailureException.throwEx("fail"); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + User user = operations.findById(User.class).one(id); + assertNull(user); + } + + @Service + @Component + @EnableTransactionManagement + static class UserService { + @Autowired UserRepository userRepo; + + @Transactional + public void run(Consumer callback) { + callback.accept(userRepo); + } + + @Transactional + public List findByFirstname(String name) { + return userRepo.findByFirstname(name); + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java new file mode 100644 index 000000000..06a15a9d3 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java @@ -0,0 +1,503 @@ +/* + * Copyright 2012-2021 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.transactions; + +import com.couchbase.client.core.error.transaction.AttemptExpiredException; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +/** + * Tests for @Transactional, using template methods (findById etc.) + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalTemplateIntegrationTests.PersonService.class }) +public class CouchbaseTransactionalTemplateIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + // Skip this as we just one to track TransactionContext + List pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + + List pwovr = operations.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all(); + List pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + Person inserted = personService.doInTransaction(tryCount, (ops) -> { + return ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + }); + + Person fetched = operations.findById(Person.class).one(inserted.id()); + assertEquals(inserted.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).one(WalterWhite); + + personService.fetchAndReplace(person.id(), tryCount, (p) -> { + p.setFirstname("changed"); + return p; + }); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).one(WalterWhite); + + personService.fetchAndRemove(person.id(), tryCount); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + List removed = personService.doInTransaction(tryCount, ops -> { + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + assertEquals(1, removed.size()); + } + + @DisplayName("A basic golden path findByQuery should succeed (though we don't know for sure it executed transactionally)") + @Test + public void committedFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + List found = personService.doInTransaction(tryCount, ops -> { + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + }); + + assertEquals(1, found.size()); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger tryCount = new AtomicInteger(); + AtomicReference id = new AtomicReference<>(); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + ops.insertById(Person.class).one(WalterWhite); + id.set(WalterWhite.id()); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).one(id.get()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.replaceById(Person.class).one(p.withFirstName("changed")); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertEquals(person.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = operations.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).oneEntity(p); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, ops -> { + ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + assertEquals(1, tryCount.get()); + } + + @Test + public void shouldRollbackAfterException() { + assertThrowsWithCause(() -> { + personService.insertThenThrow(); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = personService.declarativeSavePerson(WalterWhite); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void concurrentTxns() { + Runnable r = () -> { + Thread t = Thread.currentThread(); + System.out.printf("Started thread %d %s%n", t.getId(), t.getName()); + Person p = personService.declarativeSavePersonWithThread(WalterWhite, t); + System.out.printf("Finished thread %d %s%n", t.getId(), t.getName()); + }; + List threads = new ArrayList<>(); + for (int i = 0; i < 50; i++) { // somewhere between 50-80 it starts to hang + Thread t = new Thread(r); + t.start(); + threads.add(t); + } + + threads.forEach(t -> { + try { + System.out.printf("Waiting for thread %d %s%n", t.getId(), t.getName()); + t.join(); + System.out.printf("Finished waiting for thread %d %s%n", t.getId(), t.getName()); + } catch (InterruptedException e) { + fail(); // interrupted + } + }); + } + + @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.") + @Test + public void replacePerson() { + Person person = operations.insertById(Person.class).one(WalterWhite); + Person refetched = operations.findById(Person.class).one(person.id()); + operations.replaceById(Person.class).one(refetched); + assertNotEquals(person.getVersion(), refetched.getVersion()); + AtomicInteger tryCount = new AtomicInteger(0); + assertThrowsWithCause(() -> personService.replace(person, tryCount), + TransactionSystemUnambiguousException.class, AttemptExpiredException.class); + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).one(person); + + assertThrowsWithCause(() -> personService.replaceEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = operations.insertById(Person.class).one(WalterWhite); + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person( "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount), TransactionSystemUnambiguousException.class, + IllegalArgumentException.class); + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).one(person); + + assertThrowsWithCause(() -> personService.removeEntityWithoutVersion(person.id()), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = operations.insertById(Person.class).one(WalterWhite); + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).one(p.id()); + return p; + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + final CouchbaseOperations personOperations; + final ReactiveCouchbaseOperations personOperationsRx; + + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx) { + personOperations = ops; + personOperationsRx = opsRx; + } + + @Transactional + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out + .println(String.format("Thread %d %s", Thread.currentThread().getId(), Thread.currentThread().getName())); + Person ret = personOperations.insertById(Person.class).one(person); + System.out.println(String.format("Thread %d (was %d) %s", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName())); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public Person declarativeSavePersonWithThread(Person person, Thread thread) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out.printf("Thread %d %s, started from %d %s%n", Thread.currentThread().getId(), + Thread.currentThread().getName(), thread.getId(), thread.getName()); + Person ret = personOperations.insertById(Person.class).one(person); + System.out.printf("Thread %d (was %d) %s, started from %d %s%n", Thread.currentThread().getId(), currentThreadId, + Thread.currentThread().getName(), thread.getId(), thread.getName()); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional + public void insertThenThrow() { + assertInAnnotationTransaction(true); + Person person = personOperations.insertById(Person.class).one(new Person( "Walter", "White")); + SimulateFailureException.throwEx(); + } + + @Autowired + CouchbaseCallbackTransactionManager callbackTm; + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional + public Person replacePerson(Person person, AtomicInteger tryCount) { + tryCount.incrementAndGet(); + // Note that passing in a Person and replace it in this way, is not supported + return personOperations.replaceById(Person.class).one(person); + } + + @Transactional + public void replaceEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id); + personOperations.replaceById(PersonWithoutVersion.class).one(fetched); + } + + @Transactional + public void removeEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id); + personOperations.removeById(PersonWithoutVersion.class).oneEntity(fetched); + } + + @Transactional + public Person declarativeFindReplaceTwicePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.id()); + Person pUpdated = personOperations.replaceById(Person.class).one(p); + return personOperations.replaceById(Person.class).one(pUpdated); + } + + @Transactional(timeout = 2) + + public Person replace(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return personOperations.replaceById(Person.class).one(person); + } + + @Transactional + public Person fetchAndReplace(String id, AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).one(id); + Person modified = callback.apply(p); + return personOperations.replaceById(Person.class).one(modified); + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + + @Transactional + public void fetchAndRemove(String id, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).one(id); + personOperations.removeById(Person.class).oneEntity(p); + } + + } + + static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in transaction = " + (!inTransaction) + " but expected in annotation transaction = " + inTransaction); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java new file mode 100644 index 000000000..1099f79e8 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012-2022 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.transactions; + +import com.couchbase.client.java.kv.InsertOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.kv.ReplaceOptions; +import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, where parameters/options are being set that aren't support in a transaction. These + * will be rejected at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalUnsettableParametersIntegrationTests.PersonService.class }) +public class CouchbaseTransactionalUnsettableParametersIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + + Person WalterWhite; + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + } + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + void test(Consumer r) { + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + r.accept(ops); + return null; + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using insertById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void insertWithDurability() { + test((ops) -> { + ops.insertById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(WalterWhite); + }); + } + + @DisplayName("Using insertById().withExpiry in a transaction is rejected at runtime") + @Test + public void insertWithExpiry() { + test((ops) -> { + ops.insertById(Person.class).withExpiry(Duration.ofSeconds(3)).one(WalterWhite); + }); + } + + @DisplayName("Using insertById().withOptions in a transaction is rejected at runtime") + @Test + public void insertWithOptions() { + test((ops) -> { + ops.insertById(Person.class).withOptions(InsertOptions.insertOptions()).one(WalterWhite); + }); + } + + @DisplayName("Using replaceById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void replaceWithDurability() { + test((ops) -> { + ops.replaceById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(WalterWhite); + }); + } + + @DisplayName("Using replaceById().withExpiry in a transaction is rejected at runtime") + @Test + public void replaceWithExpiry() { + test((ops) -> { + ops.replaceById(Person.class).withExpiry(Duration.ofSeconds(3)).one(WalterWhite); + }); + } + + @DisplayName("Using replaceById().withOptions in a transaction is rejected at runtime") + @Test + public void replaceWithOptions() { + test((ops) -> { + ops.replaceById(Person.class).withOptions(ReplaceOptions.replaceOptions()).one(WalterWhite); + }); + } + + @DisplayName("Using removeById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void removeWithDurability() { + test((ops) -> { + ops.removeById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).oneEntity(WalterWhite); + }); + } + + @DisplayName("Using removeById().withOptions in a transaction is rejected at runtime") + @Test + public void removeWithOptions() { + test((ops) -> { + ops.removeById(Person.class).withOptions(RemoveOptions.removeOptions()).oneEntity(WalterWhite); + }); + } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + final CouchbaseOperations personOperations; + + public PersonService(CouchbaseOperations ops) { + personOperations = ops; + } + + @Transactional + public T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java new file mode 100644 index 000000000..94f5181f1 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2022 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.transactions; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * We do not support direct use of the PlatformTransactionManager. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class DirectPlatformTransactionManagerIntegrationTests extends JavaIntegrationTests { + @Autowired + CouchbaseClientFactory couchbaseClientFactory; + + @Test + public void directUseAlwaysFails() { + PlatformTransactionManager ptm = new CouchbaseCallbackTransactionManager(couchbaseClientFactory); + + assertThrowsWithCause(() -> { + TransactionDefinition def = new DefaultTransactionDefinition(); + ptm.getTransaction(def); + }, UnsupportedOperationException.class); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java new file mode 100644 index 000000000..23ce78c6d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java @@ -0,0 +1,14 @@ +package org.springframework.data.couchbase.transactions; + +import java.util.UUID; + +public class ObjectId{ + public ObjectId(){ + id = UUID.randomUUID().toString(); + } + String id; + + public String toString(){ + return id.toString(); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java new file mode 100644 index 000000000..0d4b694a0 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java @@ -0,0 +1,179 @@ +package org.springframework.data.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.springframework.data.couchbase.util.JavaIntegrationTests.throwSimulateFailureException; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; + +@Service +@Component +@EnableTransactionManagement +class PersonService { + + final CouchbaseOperations personOperations; + final ReactiveCouchbaseOperations personOperationsRx; + final TransactionalOperator transactionalOperator; + + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx, + TransactionalOperator transactionalOperator) { + personOperations = ops; + personOperationsRx = opsRx; + this.transactionalOperator = transactionalOperator; + } + + public Person savePersonErrors(Person person) { + assertInAnnotationTransaction(false); + + return personOperationsRx.insertById(Person.class).one(person)// + . flatMap(it -> Mono.error(new SimulateFailureException()))// + .as(transactionalOperator::transactional).block(); + } + + public Person savePerson(Person person) { + assertInAnnotationTransaction(false); + return personOperationsRx.insertById(Person.class).one(person)// + .as(transactionalOperator::transactional).block(); + } + + public Long countDuringTx(Person person) { + assertInAnnotationTransaction(false); + return personOperationsRx.insertById(Person.class).one(person)// + .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) + .as(transactionalOperator::transactional).block(); + } + + public List saveWithLogs(Person person) { + assertInAnnotationTransaction(false); + return Flux + .merge( + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")), + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")), + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), + personOperationsRx.insertById(Person.class).one(person), + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .withConsistency(REQUEST_PLUS).all()) // + .as(transactionalOperator::transactional).collectList().block(); + + } + + public List saveWithErrorLogs(Person person) { + assertInAnnotationTransaction(false); + + return Flux + .merge( + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), + // + personOperationsRx.insertById(Person.class).one(person), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .withConsistency(REQUEST_PLUS).all()) // + . flatMap(it -> Mono.error(new SimulateFailureException())) + .as(transactionalOperator::transactional).collectList().block(); + + } + + // org.springframework.beans.factory.NoUniqueBeanDefinitionException: + // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single + // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePersonErrors(Person person) { + assertInAnnotationTransaction(true); + Person p = personOperations.insertById(Person.class).one(person); // + SimulateFailureException.throwEx(); + return p; + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.id()); + ReplaceLoopThread.updateOutOfTransaction(personOperations, person, tryCount.get()); + return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); + } + + /** + * The ReactiveCouchbaseTransactionManager does not retry on write-write conflict. Instead it will throw + * RetryTransactionException to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // must use transactionalOperator + public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { + //assertInAnnotationTransaction(true); + return personOperationsRx.findById(Person.class).one(person.id()) + .map((p) -> ReplaceLoopThread.updateOutOfTransaction(personOperations, p, tryCount.incrementAndGet())) + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))).as(transactionalOperator::transactional); + } + + /** + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + ReplaceLoopThread.updateOutOfTransaction(personOperations, person, tryCount.get()); + return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); + } + + //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // must use transactionalOperator + public Mono declarativeSavePersonReactive(Person person) { + // assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class).one(person).as(transactionalOperator::transactional); + } + + //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // must use transactionalOperator + public Mono declarativeSavePersonErrorsReactive(Person person) { + //assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class).one(person).map((pp) -> throwSimulateFailureException(pp)).as(transactionalOperator::transactional); // + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java new file mode 100644 index 000000000..d4b179c63 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java @@ -0,0 +1,93 @@ +package org.springframework.data.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; + +// @RequiredArgsConstructor +class PersonServiceReactive { + + final ReactiveCouchbaseOperations personOperationsRx; + final CouchbaseOperations personOperations; + final TransactionalOperator transactionalOperator; + + public PersonServiceReactive(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx, + TransactionalOperator transactionalOperator) { + this.personOperations = ops; + this.personOperationsRx = opsRx; + this.transactionalOperator = transactionalOperator; + return; + } + + public Mono savePersonErrors(Person person) { + return personOperationsRx.insertById(Person.class).one(person) // + . flatMap(it -> Mono.error(new SimulateFailureException())) // + .as(transactionalOperator::transactional); + } + + public Mono savePerson(Person person) { + return personOperationsRx.insertById(Person.class).one(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + public Mono countDuringTx(Person person) { + return personOperationsRx.save(person) // + .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithLogs(Person person) { + return Flux + .merge( + personOperationsRx.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog( + new ObjectId().toString(), "beforeConvert")), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterConvert")), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), + personOperationsRx.save(person), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionReactiveIntegrationTests.EventLog.class) + .withConsistency(REQUEST_PLUS).all()) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithErrorLogs(Person person) { + return Flux + .merge( + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeConvert")), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterConvert")), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), + personOperationsRx.save(person), + personOperationsRx + .save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // + . flatMap(it -> Mono.error(new SimulateFailureException())) // + .as(transactionalOperator::transactional); + } + + // @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Flux declarativeSavePerson(Person person) { + return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Flux declarativeSavePersonErrors(Person person) { + Person p = personOperations.insertById(Person.class).one(person); + Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); + SimulateFailureException.throwEx(); // so the following lines is not flagged as unreachable + return Flux.just(p); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java new file mode 100644 index 000000000..fc6b41029 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2021 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.transactions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.TransactionalSupport; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for reactive @Transactional, using the CouchbaseTransactionInterceptor. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + ReactiveTransactionalTemplateIntegrationTests.PersonService.class }) +public class ReactiveTransactionalTemplateIntegrationTests extends JavaIntegrationTests { + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate blocking; + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + assertNotInTransaction(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + + personService.doInTransaction(tryCount, (ops) -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite); + }); + }).block(); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger tryCount = new AtomicInteger(); + + assertThrowsWithCause(() -> { + personService.doInTransaction(tryCount, (ops) -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite).then(Mono.error(new SimulateFailureException())); + }); + }).block(); + }, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Forcing CAS mismatch causes a transaction retry") + @Test + public void casMismatchCausesRetry() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + personService.doInTransaction(attempts, ops -> { + return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.fromRunnable(() -> { + ReplaceLoopThread.updateOutOfTransaction(blocking, person.withFirstName("ChangedExternally"), attempts.get()); + }).then(ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")))); + }).block(); + + Person fetched = blocking.findById(Person.class).one(person.getId().toString()); + assertEquals("Changed by transaction", fetched.getFirstname()); + assertEquals(2, attempts.get()); + } + + @Test + public void returnMono() { + AtomicInteger tryCount = new AtomicInteger(0); + + Person fromLambda = personService.doInTransactionReturningMono(tryCount, (ops) -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite).log("source"); + }).log("returnMono test"); + }).block(); + + assertNotNull(fromLambda); + assertEquals(WalterWhite.getFirstname(), fromLambda.getFirstname()); + } + + @Test + public void returnFlux() { + AtomicInteger tryCount = new AtomicInteger(0); + + List fromLambda = personService.doInTransactionReturningFlux(tryCount, (ops) -> { + return Flux.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite) + .thenMany(Flux.fromIterable(Arrays.asList(1, 2, 3)).log("1")); + }); + }).collectList().block(); + + assertEquals(3, fromLambda.size()); + } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + final ReactiveCouchbaseOperations ops; + + public PersonService(ReactiveCouchbaseOperations ops) { + this.ops = ops; + } + + @Transactional + public Mono doInTransaction(AtomicInteger tryCount, Function> callback) { + return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(stat -> { + assertTrue(stat.isPresent(), "Not in transaction"); + tryCount.incrementAndGet(); + return callback.apply(ops).then(); + }); + } + + @Transactional + public Mono doInTransactionReturningMono(AtomicInteger tryCount, + Function> callback) { + return Mono.defer(() -> { + tryCount.incrementAndGet(); + return callback.apply(ops); + }); + } + + @Transactional + public Flux doInTransactionReturningFlux(AtomicInteger tryCount, + Function> callback) { + return Flux.defer(() -> { + tryCount.incrementAndGet(); + return callback.apply(ops); + }); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java new file mode 100644 index 000000000..47895b7d2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java @@ -0,0 +1,68 @@ +package org.springframework.data.couchbase.transactions; + +import org.junit.Assert; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.util.JavaIntegrationTests; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ReplaceLoopThread extends Thread { + private final CouchbaseOperations couchbaseOperations; + AtomicBoolean stop = new AtomicBoolean(false); + UUID id; + int maxIterations = 100; + + public ReplaceLoopThread(CouchbaseOperations couchbaseOperations, UUID id, int... iterations) { + Assert.assertNotNull("couchbaseOperations cannot be null", couchbaseOperations); + this.couchbaseOperations = couchbaseOperations; + this.id = id; + if (iterations != null && iterations.length == 1) { + this.maxIterations = iterations[0]; + } + } + + public void run() { + for (int i = 0; i < maxIterations && !stop.get(); i++) { + JavaIntegrationTests.sleepMs(10); + try { + // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. + // annotations processed so we just check getFirstname().equals() + // switchedPerson has version=0, so it doesn't check CAS + Person fetched = couchbaseOperations.findById(Person.class).one(id.toString()); + couchbaseOperations.replaceById(Person.class).one(fetched.withFirstName("Changed externally")); + System.out.println("********** replace thread: " + i + " success"); + } catch (Exception e) { + System.out.println("********** replace thread: " + i + " " + e.getClass() + .getName()); + e.printStackTrace(); + } + } + + } + + public void setStopFlag() { + stop.set(true); + } + + public static Person updateOutOfTransaction(CouchbaseOperations couchbaseOperations, Person pp, int tryCount) { + System.err.println("updateOutOfTransaction: "+tryCount); + if (tryCount < 1) { + throw new RuntimeException("increment before calling updateOutOfTransactions"); + } + if (tryCount > 1) { + return pp; + } + ReplaceLoopThread t = new ReplaceLoopThread(couchbaseOperations, + pp.getId(), 1); + t.start(); + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return pp; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java new file mode 100644 index 000000000..423cb3dc3 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.transactions; + +public class SimulateFailureException extends RuntimeException { + + public SimulateFailureException(String... s){ + super(s!= null && s.length > 0 ? s[0] : null); + } + + public SimulateFailureException(){} + + public static void throwEx(String... s){ + throw new SimulateFailureException(s); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java new file mode 100644 index 000000000..2f56e4e49 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java @@ -0,0 +1,381 @@ +/* + * Copyright 2012-2022 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.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +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.assertTrue; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import com.couchbase.client.java.query.QueryScanConsistency; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.transaction.support.TransactionTemplate; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for Spring's TransactionTemplate, used CouchbaseCallbackTransactionManager, using template methods + * (findById etc.) + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class TransactionTemplateIntegrationTests extends JavaIntegrationTests { + TransactionTemplate template; + @Autowired + CouchbaseCallbackTransactionManager transactionManager; + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseTemplate ops; + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + assertNotInTransaction(); + List rp0 = ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = ops.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all(); + + template = new TransactionTemplate(transactionManager); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + static class RunResult { + public final int attempts; + + public RunResult(int attempts) { + this.attempts = attempts; + } + } + + private RunResult doInTransaction(Consumer lambda) { + AtomicInteger tryCount = new AtomicInteger(); + + template.executeWithoutResult(status -> { + TransactionTestUtil.assertInTransaction(); + assertFalse(status.hasSavepoint()); + assertFalse(status.isRollbackOnly()); + assertFalse(status.isCompleted()); + assertTrue(status.isNewTransaction()); + + tryCount.incrementAndGet(); + lambda.accept(status); + }); + + TransactionTestUtil.assertNotInTransaction(); + + return new RunResult(tryCount.get()); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + RunResult rr = doInTransaction(status -> { + ops.insertById(Person.class).one(WalterWhite); + }); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(status -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.replaceById(Person.class).one(p.withFirstName("changed")); + }); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(status -> { + Person fetched = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).oneEntity(fetched); + }); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(status -> { + List removed = ops.removeByQuery(Person.class) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + assertEquals(1, removed.size()); + }); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path findByQuery should succeed") + @Test + public void committedFindByQuery() { + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(status -> { + List found = ops.findByQuery(Person.class) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + assertEquals(1, found.size()); + }); + + assertEquals(1, rr.attempts); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger attempts = new AtomicInteger(); + + assertThrowsWithCause(() -> doInTransaction(status -> { + attempts.incrementAndGet(); + Person person = ops.insertById(Person.class).one(WalterWhite); + throw new SimulateFailureException(); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(status -> { + attempts.incrementAndGet(); + Person p = ops.findById(Person.class).one(person.id()); + p.setFirstname("changed"); + ops.replaceById(Person.class).one(p); + throw new SimulateFailureException(); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertEquals(person.getFirstname(), fetched.getFirstname()); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(status -> { + attempts.incrementAndGet(); + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).oneEntity(p); + throw new SimulateFailureException(); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> doInTransaction(status -> { + attempts.incrementAndGet(); + ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + throw new SimulateFailureException(); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> doInTransaction(status -> { + attempts.incrementAndGet(); + ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + throw new SimulateFailureException(); + }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + + assertEquals(1, attempts.get()); + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White")); + assertThrowsWithCause(() -> { + doInTransaction(status -> { + PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id()); + ops.replaceById(PersonWithoutVersion.class).one(fetched); + }); + }, TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + + assertThrowsWithCause(() -> doInTransaction(status -> { + ops.replaceById(Person.class).one(switchedPerson); + }), TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White")); + assertThrowsWithCause(() -> doInTransaction(status -> { + PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id()); + ops.removeById(PersonWithoutVersion.class).oneEntity(fetched); + }), TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(status -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).one(p.id()); + }), TransactionSystemUnambiguousException.class, IllegalArgumentException.class); + } + + @DisplayName("setRollbackOnly should cause a rollback") + @Test + public void setRollbackOnly() { + + assertThrowsWithCause(() -> doInTransaction(status -> { + status.setRollbackOnly(); + Person person = ops.insertById(Person.class).one(WalterWhite); + }), TransactionSystemUnambiguousException.class); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertNull(fetched); + } + + @DisplayName("Setting an unsupported isolation level should fail") + @Test + public void unsupportedIsolationLevel() { + template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + + assertThrowsWithCause(() -> doInTransaction(status -> {}), IllegalArgumentException.class); + } + + @DisplayName("Setting PROPAGATION_MANDATORY should fail, as not in a transaction") + @Test + public void propagationMandatoryOutsideTransaction() { + template.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); + + assertThrowsWithCause(() -> doInTransaction(status -> {}), IllegalTransactionStateException.class); + } + + @Test + public void nestedTransactionTemplates() { + TransactionTemplate template2 = new TransactionTemplate(transactionManager); + template2.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); + + template.executeWithoutResult(status -> { + template2.executeWithoutResult(status2 -> { + Person person = ops.insertById(Person.class).one(WalterWhite); + }); + }); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertEquals("Walter", fetched.getFirstname()); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java new file mode 100644 index 000000000..739dafd7d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java @@ -0,0 +1,46 @@ +package org.springframework.data.couchbase.transactions; + +import com.couchbase.client.java.env.ClusterEnvironment; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import java.time.Duration; + + +@Configuration +@EnableCouchbaseRepositories("org.springframework.data.couchbase") +@EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") +@EnableTransactionManagement +public class TransactionsConfig extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return ClusterAwareIntegrationTests.connectionString(); + } + + @Override + public String getUserName() { + return ClusterAwareIntegrationTests.config().adminUsername(); + } + + @Override + public String getPassword() { + return ClusterAwareIntegrationTests.config().adminPassword(); + } + + @Override + public String getBucketName() { + return ClusterAwareIntegrationTests.bucketName(); + } + + @Override + public void configureEnvironment(ClusterEnvironment.Builder builder) { + // twenty minutes for debugging in the debugger. + builder.transactionsConfig(com.couchbase.client.java.transactions.config.TransactionsConfig.builder().timeout(Duration.ofMinutes(20))); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java new file mode 100644 index 000000000..744b203be --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2022 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.transactions.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import reactor.core.publisher.Mono; + +/** + * Tests for regular reactive SDK transactions, where Spring operations that aren't supported in a transaction are being used. + * They should be prevented at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = {TransactionsConfig.class, SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.PersonService.class}) +public class SDKReactiveTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + } + + void test(Function> r) { + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> { + couchbaseClientFactory.getCluster().reactive().transactions().run(ignored -> { + return personService.doInService(tryCount, (ops) -> { + return r.apply(ops); + }); + }).block(); + }, TransactionFailedException.class, IllegalArgumentException.class); + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using existsById() in a transaction is rejected at runtime") + @Test + public void existsById() { + test((ops) -> { + return ops.existsById(Person.class).one(WalterWhite.id()); + }); + } + + @DisplayName("Using findByAnalytics() in a transaction is rejected at runtime") + @Test + public void findByAnalytics() { + test((ops) -> { + return ops.findByAnalytics(Person.class).one(); + }); + } + + @DisplayName("Using findFromReplicasById() in a transaction is rejected at runtime") + @Test + public void findFromReplicasById() { + test((ops) -> { + return ops.findFromReplicasById(Person.class).any(WalterWhite.id()); + }); + } + + @DisplayName("Using upsertById() in a transaction is rejected at runtime") + @Test + public void upsertById() { + test((ops) -> { + return ops.upsertById(Person.class).one(WalterWhite); + }); + } + + // This is intentionally not a @Transactional service + @Service + @Component + static + class PersonService { + final ReactiveCouchbaseOperations personOperations; + + public PersonService(ReactiveCouchbaseOperations ops) { + personOperations = ops; + } + + public Mono doInService(AtomicInteger tryCount, Function> callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java new file mode 100644 index 000000000..0dbad3aba --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2012-2022 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.transactions.sdk; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.data.couchbase.transactions.ReplaceLoopThread.updateOutOfTransaction; + +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import reactor.core.publisher.Mono; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for ReactiveTransactionsWrapper, moved from CouchbasePersonTransactionIntegrationTests. + * Now ReactiveTransactionsWrapper is removed, these are testing the same operations inside a regular SDK transaction. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class SDKReactiveTransactionsPersonIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired CouchbaseTemplate operations; + + String sName = "_default"; + String cName = "_default"; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person( "Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + List rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = operations.removeByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + List p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p1 = operations.findByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + } + + @AfterEach + public void afterEachTest() { + TransactionTestUtil.assertNotInTransaction(); + } + + @Test + // need to fix this to make it deliberately have the CasMismatch by synchronization. + // And to *not* do any out-of-tx updates after the tx update has succeeded. + // And to have the tx update to a different name than the out-of-tx update + public void wrapperReplaceWithCasConflictResolvedViaRetryReactive() { + AtomicInteger tryCount = new AtomicInteger(); + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions() + .run(ctx -> rxCBTmpl.findById(Person.class).one(person.id()) // + .map((pp) -> updateOutOfTransaction(cbTmpl, pp, tryCount.incrementAndGet())) + .flatMap(ppp -> rxCBTmpl.replaceById(Person.class).one(ppp.withFirstName("Dave")))); + TransactionResult txResult = result.block(); + + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + System.err.println("pFound " + pFound); + assertEquals(2, tryCount.get(), "should have been two tries. tries: " + tryCount.get()); + assertEquals("Dave", pFound.getFirstname(), "should have been changed"); + + } + + @DisplayName("Forcing CAS mismatch causes a transaction retry") + @Test + public void casMismatchCausesRetry() { + Person person = operations.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + couchbaseClientFactory.getCluster().reactive().transactions() + .run(ctx -> rxCBTmpl.findById(Person.class).one(person.id()).flatMap(fetched -> { + ReplaceLoopThread.updateOutOfTransaction(cbTmpl, fetched, attempts.incrementAndGet()); + return rxCBTmpl.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")); + })).block(); + + Person fetched = operations.findById(Person.class).one(person.id()); + assertEquals("Changed by transaction", fetched.getFirstname()); + assertEquals(2, attempts.get()); + } + + @Test + public void replacePersonCBTransactionsRxTmplRollback() { + String newName = "Walt"; + Person person = cbTmpl.insertById(Person.class).one(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // + return rxCBTmpl.findById(Person.class).one(person.id()) // + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty()); + }); + result.block(); + Person pFound = cbTmpl.findById(Person.class).one(person.id()); + System.err.println(pFound); + assertEquals(newName, pFound.getFirstname()); + } + + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then(); + }); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test // ok + public void deletePersonCBTransactionsRxTmplFail() { + Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = repo.withCollection(cName).save(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).delete(person).then(); + }); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = repo.withCollection(cName).save(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).findById(person.id()) + .flatMap(pp -> rxRepo.withCollection(cName).delete(pp).then(rxRepo.withCollection(cName).delete(pp))).then(); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertEquals(pFound, person, "Should have found " + person + " instead of " + pFound); + } + + @Test + public void findPersonCBTransactions() { + Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName) + .one(WalterWhite); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + return rxCBTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).matching(q) + .withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> { + System.err.println("doc: " + doc); + docs.add(doc); + }); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person + " instead of " + o); + } + } + + @Test + public void insertPersonRbCBTransactions() { + Person person = WalterWhite; + Mono result = couchbaseClientFactory.getCluster().reactive().transactions() + .run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void replacePersonRbCBTransactions() { + Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> // + rxCBTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.id()) // + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inScope(sName).inCollection(cName) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.id()); + assertEquals(person, pFound, "Should have found " + person + " instead of " + pFound); + } + + @Test + public void findPersonSpringTransactions() { + Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> rxCBTmpl.findByQuery(Person.class) + .inScope(sName).inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java new file mode 100644 index 000000000..9b5563b38 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2012-2021 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.transactions.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.TransactionalSupport; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests using template methods (findById etc.) inside a regular reactive SDK transaction. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class SDKReactiveTransactionsTemplateIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTemplate ops; + @Autowired CouchbaseTemplate blocking; + + Person WalterWhite; + + @BeforeEach + public void beforeEachTest(){ + WalterWhite = new Person ("Walter", "White"); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + static class RunResult { + public final TransactionResult result; + public final int attempts; + + public RunResult(TransactionResult result, int attempts) { + this.result = result; + this.attempts = attempts; + } + } + + private RunResult doInTransaction(Function> lambda) { + return doInTransaction(lambda, null); + } + + private RunResult doInTransaction(Function> lambda, + @Nullable TransactionOptions options) { + AtomicInteger attempts = new AtomicInteger(); + + TransactionResult result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + return TransactionalSupport.checkForTransactionInThreadLocalStorage().then(Mono.defer(() -> { + attempts.incrementAndGet(); + return lambda.apply(ctx); + })); + }, options).block(); + + assertNotInTransaction(); + + return new RunResult(result, attempts.get()); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + + RunResult rr = doInTransaction(ctx -> { + return Mono.defer(() -> { + return ops.insertById(Person.class).one(WalterWhite); + }); + }); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertEquals("Walter", fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + + Person initial = blocking.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(ctx -> { + return ops.findById(Person.class).one(WalterWhite.id()).flatMap(person -> { + person.setFirstname("changed"); + return ops.replaceById(Person.class).one(person); + }); + }); + + Person fetched = blocking.findById(Person.class).one(initial.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(ctx -> { + return ops.findById(Person.class).one(person.id()) + .flatMap(fetched -> ops.removeById(Person.class).oneEntity(fetched)); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + + Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(ctx -> { + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.id())) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().then(); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path findByQuery should succeed") + @Test + public void committedFindByQuery() { + Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(ctx -> { + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.getFirstname())).all().then(); + }); + + assertEquals(1, rr.attempts); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger attempts = new AtomicInteger(); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + attempts.incrementAndGet(); + return ops.insertById(Person.class).one(WalterWhite).map((p) -> throwSimulateFailureException(p)); + }), TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(WalterWhite.id()); + assertNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + attempts.incrementAndGet(); + return ops.findById(Person.class).one(person.id()) // + .flatMap(p -> ops.replaceById(Person.class).one(p.withFirstName("changed"))) // + .map(p -> throwSimulateFailureException(p)); + }), TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertEquals("Walter", fetched.getFirstname()); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + attempts.incrementAndGet(); + return ops.findById(Person.class).one(person.id()) + .flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // + .doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result + }), TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + attempts.incrementAndGet(); + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all().elementAt(0) + .map(p -> throwSimulateFailureException(p)); + }), TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + attempts.incrementAndGet(); + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all().elementAt(0) + .map(p -> throwSimulateFailureException(p)); + }), TransactionFailedException.class, SimulateFailureException.class); + + assertEquals(1, attempts.get()); + } + + @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.") + @Test + public void replacePerson() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + + Person refetched = blocking.findById(Person.class).one(person.id()); + refetched = blocking.replaceById(Person.class).one(refetched); // new cas + + assertNotEquals(person.getVersion(), refetched.getVersion()); + + assertThrowsWithCause(() -> doInTransaction(ctx -> ops.replaceById(Person.class).one(person), // old cas + TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2))), TransactionFailedException.class , + Exception.class ); + + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + ; + PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion("Walter", "White")); + assertThrowsWithCause( + () -> doInTransaction(ctx -> ops.findById(PersonWithoutVersion.class).one(person.id()) + .flatMap(fetched -> ops.replaceById(PersonWithoutVersion.class).one(fetched))), + TransactionFailedException.class, IllegalArgumentException.class); + + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + return ops.replaceById(Person.class).one(switchedPerson); + }), TransactionFailedException.class, IllegalArgumentException.class); + + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class).one(new PersonWithoutVersion("Walter", "White")); + assertThrowsWithCause(() -> doInTransaction(ctx -> { + return ops.findById(PersonWithoutVersion.class).one(person.id()) + .flatMap(fetched -> ops.removeById(PersonWithoutVersion.class).oneEntity(fetched)); + }), TransactionFailedException.class, IllegalArgumentException.class); + + } + + @DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> doInTransaction(ctx -> { + return ops.findById(Person.class).one(person.id()) + .flatMap(p -> ops.removeById(Person.class).one(p.id())); + }), TransactionFailedException.class, IllegalArgumentException.class); + + } + + @DisplayName("Forcing CAS mismatch causes a transaction retry") + @Test + public void casMismatchCausesRetry() { + Person person = blocking.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + doInTransaction(ctx -> { + return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.defer(() -> { + ReplaceLoopThread.updateOutOfTransaction(blocking, person.withFirstName("Changed externally"), + attempts.incrementAndGet()); + return ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")); + })); + }); + + Person fetched = blocking.findById(Person.class).one(person.id()); + assertEquals("Changed by transaction", fetched.getFirstname()); + assertEquals(2, attempts.get()); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java new file mode 100644 index 000000000..a1f3005b6 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2022 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.transactions.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for regular SDK transactions, where Spring operations that aren't supported in a transaction are being used. + * They should be prevented at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = {TransactionsConfig.class, SDKTransactionsNonAllowableOperationsIntegrationTests.PersonService.class}) +public class SDKTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired PersonService personService; + + Person WalterWhite; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + TransactionTestUtil.assertNotInTransaction(); + } + + void test(Consumer r) { + AtomicInteger tryCount = new AtomicInteger(0); + + assertThrowsWithCause(() -> { + couchbaseClientFactory.getCluster().transactions().run(ignored -> { + personService.doInService(tryCount, (ops) -> { + r.accept(ops); + return null; + }); + }); + }, TransactionFailedException.class, IllegalArgumentException.class); + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using existsById() in a transaction is rejected at runtime") + @Test + public void existsById() { + test((ops) -> { + ops.existsById(Person.class).one(WalterWhite.id()); + }); + } + + @DisplayName("Using findByAnalytics() in a transaction is rejected at runtime") + @Test + public void findByAnalytics() { + test((ops) -> { + ops.findByAnalytics(Person.class).one(); + }); + } + + @DisplayName("Using findFromReplicasById() in a transaction is rejected at runtime") + @Test + public void findFromReplicasById() { + test((ops) -> { + ops.findFromReplicasById(Person.class).any(WalterWhite.id()); + }); + } + + @DisplayName("Using upsertById() in a transaction is rejected at runtime") + @Test + public void upsertById() { + test((ops) -> { + ops.upsertById(Person.class).one(WalterWhite); + }); + } + + // This is intentionally not a @Transactional service + @Service + @Component + static + class PersonService { + final CouchbaseOperations personOperations; + + public PersonService(CouchbaseOperations ops) { + personOperations = ops; + } + + public T doInService(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java new file mode 100644 index 000000000..5a2623f0b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java @@ -0,0 +1,422 @@ +/* + * Copyright 2012-2022 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.transactions.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import com.couchbase.client.core.error.transaction.PreviousOperationFailedException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; +import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for using template methods (findById etc.) inside a regular SDK transaction. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(TransactionsConfig.class) +public class SDKTransactionsTemplateIntegrationTests extends JavaIntegrationTests { + // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseTemplate ops; + + Person WalterWhite; + + @BeforeEach + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); + assertNotInTransaction(); + } + + @AfterEach + public void afterEachTest() { + assertNotInTransaction(); + } + + static class RunResult { + public final TransactionResult result; + public final int attempts; + + public RunResult(TransactionResult result, int attempts) { + this.result = result; + this.attempts = attempts; + } + } + + private RunResult doInTransaction(Consumer lambda) { + return doInTransaction(lambda, null); + } + + private RunResult doInTransaction(Consumer lambda, + @Nullable TransactionOptions options) { + AtomicInteger attempts = new AtomicInteger(); + + TransactionResult result = couchbaseClientFactory.getCluster().transactions().run(ctx -> { + assertInTransaction(); + attempts.incrementAndGet(); + lambda.accept(ctx); + }, options); + + assertNotInTransaction(); + + return new RunResult(result, attempts.get()); + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + + RunResult rr = doInTransaction(ctx -> { + ops.insertById(Person.class).one(WalterWhite); + }); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + Person p = ops.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(ctx -> { + Person person = ops.findById(Person.class).one(p.id()); + person.setFirstname("changed"); + ops.replaceById(Person.class).one(person); + }); + + Person fetched = ops.findById(Person.class).one(p.id()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + RunResult rr = doInTransaction(ctx -> { + Person fetched = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).oneEntity(fetched); + }); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path removeByQuery should succeed") + @Test + public void committedRemoveByQuery() { + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(ctx -> { + List removed = ops.removeByQuery(Person.class) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + assertEquals(1, removed.size()); + }); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNull(fetched); + assertEquals(1, rr.attempts); + } + + @DisplayName("A basic golden path findByQuery should succeed") + @Test + public void committedFindByQuery() { + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + RunResult rr = doInTransaction(ctx -> { + List found = ops.findByQuery(Person.class) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + assertEquals(1, found.size()); + }); + + assertEquals(1, rr.attempts); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger attempts = new AtomicInteger(); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + attempts.incrementAndGet(); + Person person = ops.insertById(Person.class).one(WalterWhite); + throw new SimulateFailureException(); + }); + }, TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(WalterWhite.id()); + assertNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + attempts.incrementAndGet(); + Person p = ops.findById(Person.class).one(person.id()); + p.setFirstname("changed"); + ops.replaceById(Person.class).one(p); + throw new SimulateFailureException(); + }); + }, TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertEquals(WalterWhite.getFirstname(), fetched.getFirstname()); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + attempts.incrementAndGet(); + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).oneEntity(p); + throw new SimulateFailureException(); + }); + }, TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.getId().toString()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a removeByQuery then rolling back") + @Test + public void rollbackRemoveByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + attempts.incrementAndGet(); + ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionFailedException.class, SimulateFailureException.class); + + Person fetched = ops.findById(Person.class).one(person.id()); + assertNotNull(fetched); + assertEquals(1, attempts.get()); + } + + @DisplayName("Basic test of doing a findByQuery then rolling back") + @Test + public void rollbackFindByQuery() { + AtomicInteger attempts = new AtomicInteger(); + Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + attempts.incrementAndGet(); + ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); + throw new SimulateFailureException(); + }); + }, TransactionFailedException.class, SimulateFailureException.class); + + assertEquals(1, attempts.get()); + } + + @DisplayName("Create a Person outside a @Transactional block, modify it, and then replace that person in the @Transactional. The transaction will retry until timeout.") + @Test + public void replacePerson() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + Person refetched = ops.findById(Person.class).one(person.id()); + ops.replaceById(Person.class).one(refetched); + + assertNotEquals(person.getVersion(), refetched.getVersion()); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + ops.replaceById(Person.class).one(person); + }, TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2))); + }, TransactionFailedException.class); + + } + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White")); + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id()); + ops.replaceById(PersonWithoutVersion.class).one(fetched); + }); + }, TransactionFailedException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + ops.replaceById(Person.class).one(switchedPerson); + }); + }, TransactionFailedException.class, IllegalArgumentException.class); + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White")); + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id()); + ops.removeById(PersonWithoutVersion.class).oneEntity(fetched); + }); + }, TransactionFailedException.class, IllegalArgumentException.class); + } + + @DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + Person person = ops.insertById(Person.class).one(WalterWhite); + + assertThrowsWithCause(() -> { + doInTransaction(ctx -> { + Person p = ops.findById(Person.class).one(person.id()); + ops.removeById(Person.class).one(p.getId().toString()); + }); + }, TransactionFailedException.class, IllegalArgumentException.class); + } + + @DisplayName("Forcing CAS mismatch causes a transaction retry") + @Test + public void casMismatchCausesRetry() { + Person person = ops.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + doInTransaction(ctx -> { + Person fetched = ops.findById(Person.class).one(person.getId().toString()); + ReplaceLoopThread.updateOutOfTransaction(ops, person.withFirstName("Changed externally"), + attempts.incrementAndGet()); + try { + ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")); + } + catch (RuntimeException err) { + assertTrue(err instanceof UncategorizedTransactionDataAccessException); + } + + if (attempts.get() == 1) { + // Subsequent operations in this attempt should fast-fail + try { + ops.findById(Person.class).one(person.getId().toString()); + } catch (RuntimeException err) { + assertTrue(err instanceof UncategorizedTransactionDataAccessException); + assertTrue(err.getCause() instanceof PreviousOperationFailedException); + } + } + }); + + Person fetched = ops.findById(Person.class).one(person.getId().toString()); + assertEquals("Changed by transaction", fetched.getFirstname()); + assertEquals(2, attempts.get()); + } + + @DisplayName("Using the standard ctx.get(), ctx.replace() API works as expected") + @Test + public void casMismatchUsingRegularTransactionOperations() { + Person person = ops.insertById(Person.class).one(WalterWhite); + AtomicInteger attempts = new AtomicInteger(); + + doInTransaction(ctx -> { + TransactionGetResult gr = ctx.get(couchbaseClientFactory.getDefaultCollection(), WalterWhite.id()); + ReplaceLoopThread.updateOutOfTransaction(ops, person.withFirstName("Changed externally"), + attempts.incrementAndGet()); + try { + ctx.replace(gr, JsonObject.create()); + } + catch (RuntimeException err) { + assertTrue(err instanceof TransactionOperationFailedException); + } + + if (attempts.get() == 1) { + // Subsequent operations in this attempt should fast-fail + try { + ctx.get(couchbaseClientFactory.getDefaultCollection(), WalterWhite.id()); + } catch (RuntimeException err) { + assertTrue(err instanceof TransactionOperationFailedException); + assertTrue(err.getCause() instanceof PreviousOperationFailedException); + } + } + }); + + assertEquals(2, attempts.get()); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java new file mode 100644 index 000000000..98dd2b691 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 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.transactions.util; + +import org.springframework.data.couchbase.core.TransactionalSupport; +import org.springframework.transaction.NoTransactionException; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Utility methods for transaction tests. + */ +public class TransactionTestUtil { + private TransactionTestUtil() {} + + public static void assertInTransaction() { + assertTrue(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); + } + + public static void assertNotInTransaction() { + assertFalse(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 1399e0e3a..0ee40cd71 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -27,6 +27,13 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.service.Service; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -61,8 +68,13 @@ public abstract class ClusterAwareIntegrationTests { @BeforeAll static void setup(TestClusterConfig config) { testClusterConfig = config; - try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), - authenticator(), bucketName(), null, environment().build())) { + // Disabling cleanupLostAttempts to simplify output during development + ClusterEnvironment env = ClusterEnvironment.builder() + .transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false))) + .build(); + String connectString = connectionString(); + try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, + authenticator(), bucketName(), null, env)) { couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions .createPrimaryQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // this is for the N1qlJoin test diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index b63a7493c..aa39572bb 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -44,9 +44,13 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.junit.platform.commons.util.UnrecoverableExceptions; +import org.opentest4j.AssertionFailedError; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.couchbase.core.CouchbaseTemplate; @@ -84,6 +88,7 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.search.SearchQuery; import com.couchbase.client.java.search.result.SearchResult; +import org.springframework.data.couchbase.transactions.SimulateFailureException; /** * Extends the {@link ClusterAwareIntegrationTests} with java-client specific code. @@ -104,6 +109,13 @@ public static void beforeAll() { ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), + authenticator(), bucketName())) { + couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), + CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } } /** @@ -203,9 +215,8 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } if (!ready) { - createAndDeleteBucket();// need to do this because of https://issues.couchbase.com/browse/MB-50132 try { - Thread.sleep(50); + Thread.sleep(100); } catch (InterruptedException e) {} } } @@ -215,33 +226,6 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } } - private static void createAndDeleteBucket() { - final OkHttpClient httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build(); - String hostPort = connectionString().split("=")[0].replace("11210", "8091").replace("11207", "18091"); - String protocol = hostPort.equals("18091") ? "https" : "http"; - String bucketname = UUID.randomUUID().toString(); - try { - - Response postResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url(protocol + "://" + hostPort + "/pools/default/buckets/") - .post(new FormBody.Builder().add("name", bucketname).add("bucketType", "membase").add("ramQuotaMB", "100") - .add("replicaNumber", Integer.toString(0)).add("flushEnabled", "1").build()) - .build()).execute(); - - if (postResponse.code() != 202) { - throw new IOException("Could not create bucket: " + postResponse + ", Reason: " + postResponse.body().string()); - } - Response deleteResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url(protocol + "://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute(); - System.out.println("deleteResponse: " + deleteResponse); - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - /** * Improve test stability by waiting for a given service to report itself ready. */ @@ -401,4 +385,62 @@ public static void sleepMs(long ms) { Thread.sleep(ms); } catch (InterruptedException ie) {} } + + public static Throwable assertThrowsOneOf(Executable executable, Class... expectedTypes) { + + try { + executable.execute(); + } + catch (Throwable actualException) { + for(Class expectedType:expectedTypes){ + if(actualException.getClass().isAssignableFrom( expectedType)){ + return actualException; + } + } + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + String message = "Expected one of "+toString(expectedTypes)+" but was : "+actualException.getClass(); + throw new AssertionFailedError(message, actualException); + } + + String message ="Expected one of "+toString(expectedTypes)+" to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } + + private static String toString(Object[] array) { + StringBuffer sb = new StringBuffer(); + sb.append("["); + for (int i = 0; i < array.length; i++) { + if(i>0){ + sb.append(", "); + } + sb.append(array[i]); + } + sb.append("]"); + return sb.toString(); + } + + public static void assertThrowsWithCause(Executable executable, Class... expectedTypes) { + try { + executable.execute(); + } catch (Throwable actualException) { + for (Class expectedType : expectedTypes) { + if (actualException == null || !expectedType.isAssignableFrom(actualException.getClass())) { + String message = "Expected " + expectedType + " to be thrown/cause, but found " + actualException; + throw new AssertionFailedError(message, actualException); + } + actualException = actualException.getCause(); + } + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + return; + } + + String message = "Expected " + expectedTypes[0] + " to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } + + // Use this to still rely on the return type + public static T throwSimulateFailureException(T entity) { + throw new SimulateFailureException(); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index 776283df9..0ec98b390 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.util; -import static java.nio.charset.StandardCharsets.*; +import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.net.URL; @@ -83,7 +83,7 @@ private static Properties loadProperties() { defaults.load(url.openStream()); } } catch (Exception ex) { - throw new RuntimeException("Could not load properties", ex); + throw new RuntimeException("Could not load properties - maybe is pom instead of jar?", ex); } Properties all = new Properties(System.getProperties()); diff --git a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java index 2c7c2c8ea..21258c790 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java @@ -83,6 +83,7 @@ public Optional clusterCert() { * Finds the first node with a given service enabled in the config. *

    * This method can be used to find bootstrap nodes and similar. + *

    * * @param service the service to find. * @return a node config if found, empty otherwise. diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 31c07db84..857787cb9 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -146,4 +146,25 @@ public static Pair, List> comprisesNot(Iterable source, T[] al return Pair.of(unexpected, missing); } } + /** + * check if we are/are not in an @Transactional transaction + * @param inTransaction + */ + public static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") + || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); + } + } diff --git a/src/test/resources/integration.properties b/src/test/resources/integration.properties index a4c5e6f41..fcb8ed488 100644 --- a/src/test/resources/integration.properties +++ b/src/test/resources/integration.properties @@ -2,7 +2,7 @@ # If set to false, it is assumed that the host is managing the cluster and # as a result no containers or anything will be spun up. # Options: containerized, mocked, unmanaged -cluster.type=mocked +cluster.type=unmanaged # Default configs for both cases # Default configs for the mocked environment cluster.mocked.numNodes=1 diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index f8aa95196..0c076c435 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -27,5 +27,7 @@ + " + From e20ed0814e770e26ec34634429f3ac7710acd8d3 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Fri, 24 Jun 2022 17:21:45 -0700 Subject: [PATCH 18/19] Rebase with main. --- pom.xml | 7 +- .../core/AbstractTemplateSupport.java | 61 ++++-- .../core/CouchbaseTemplateSupport.java | 92 +-------- .../ExecutableFindByAnalyticsOperation.java | 101 +++++----- ...utableFindByAnalyticsOperationSupport.java | 2 +- .../core/ExecutableFindByIdOperation.java | 71 +++---- .../ExecutableFindByIdOperationSupport.java | 7 +- .../core/ExecutableFindByQueryOperation.java | 138 ++++++------- ...ExecutableFindByQueryOperationSupport.java | 10 +- .../core/ExecutableInsertByIdOperation.java | 49 ++--- .../ExecutableInsertByIdOperationSupport.java | 8 +- .../core/ExecutableRemoveByIdOperation.java | 46 ++--- .../ExecutableRemoveByIdOperationSupport.java | 8 +- .../ExecutableRemoveByQueryOperation.java | 78 ++++---- ...ecutableRemoveByQueryOperationSupport.java | 8 +- .../core/ExecutableReplaceByIdOperation.java | 42 ++-- ...ExecutableReplaceByIdOperationSupport.java | 8 +- .../core/ExecutableUpsertByIdOperation.java | 33 ++-- .../core/NonReactiveSupportWrapper.java | 4 +- .../ReactiveCouchbaseTemplateSupport.java | 4 +- .../ReactiveFindByAnalyticsOperation.java | 98 +++++----- ...activeFindByAnalyticsOperationSupport.java | 2 +- .../core/ReactiveFindByIdOperation.java | 71 +++---- .../ReactiveFindByIdOperationSupport.java | 8 +- .../core/ReactiveFindByQueryOperation.java | 127 +++++------- .../ReactiveFindByQueryOperationSupport.java | 20 +- .../core/ReactiveInsertByIdOperation.java | 49 ++--- .../ReactiveInsertByIdOperationSupport.java | 10 +- .../core/ReactiveRemoveByIdOperation.java | 43 ++--- .../ReactiveRemoveByIdOperationSupport.java | 6 - .../core/ReactiveRemoveByQueryOperation.java | 75 +++----- ...ReactiveRemoveByQueryOperationSupport.java | 11 +- .../core/ReactiveReplaceByIdOperation.java | 40 ++-- .../ReactiveReplaceByIdOperationSupport.java | 7 - .../core/ReactiveTemplateSupport.java | 4 +- .../core/ReactiveUpsertByIdOperation.java | 33 ++-- .../data/couchbase/core/TemplateSupport.java | 4 +- .../support/CouchbaseRepositoryBase.java | 9 - .../SimpleReactiveCouchbaseRepository.java | 2 +- ...gorizedTransactionDataAccessException.java | 4 +- .../cache/CouchbaseCacheIntegrationTests.java | 2 + ...mplateQueryCollectionIntegrationTests.java | 122 ++++++------ ...ouchbaseTemplateQueryIntegrationTests.java | 18 +- ...mplateQueryCollectionIntegrationTests.java | 104 +++++----- ...chbaseRepositoryQueryIntegrationTests.java | 37 ++-- ...aseRepositoryKeyValueIntegrationTests.java | 35 +--- ...chbaseRepositoryQueryIntegrationTests.java | 10 +- ...sitoryQueryCollectionIntegrationTests.java | 7 +- ...sitoryQueryCollectionIntegrationTests.java | 3 +- .../StringN1qlQueryCreatorMockedTests.java | 181 ------------------ .../query/StringN1qlQueryCreatorTests.java | 31 +-- ...basePersonTransactionIntegrationTests.java | 14 +- ...uchbaseReactiveTransactionNativeTests.java | 6 +- ...ionalOperatorTemplateIntegrationTests.java | 9 +- .../TransactionTemplateIntegrationTests.java | 30 ++- ...iveTransactionsPersonIntegrationTests.java | 12 +- ...eTransactionsTemplateIntegrationTests.java | 5 +- .../couchbase/util/JavaIntegrationTests.java | 3 + 58 files changed, 754 insertions(+), 1275 deletions(-) delete mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java diff --git a/pom.xml b/pom.xml index b0c47e34a..5e574eca8 100644 --- a/pom.xml +++ b/pom.xml @@ -245,7 +245,6 @@ compile - @@ -263,6 +262,12 @@ false + diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 5a9e3aae1..4fa6e1165 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -15,6 +15,10 @@ */ package org.springframework.data.couchbase.core; +import java.lang.reflect.InaccessibleObjectException; +import java.util.Map; +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; @@ -26,17 +30,16 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; - import org.springframework.util.ClassUtils; -import java.util.Map; -import java.util.Set; +import com.couchbase.client.core.error.CouchbaseException; public abstract class AbstractTemplateSupport { @@ -47,7 +50,8 @@ public abstract class AbstractTemplateSupport { ApplicationContext applicationContext; static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class); - public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter, TranslationService translationService) { + public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter, + TranslationService translationService) { this.template = template; this.converter = converter; this.mappingContext = converter.getMappingContext(); @@ -56,10 +60,8 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv abstract ReactiveCouchbaseTemplate getReactiveTemplate(); - public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { - final CouchbaseDocument converted = new CouchbaseDocument(id); - converted.setId(id); + public T decodeEntityBase(String id, String source, Long cas, Class entityClass, String scope, + String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { // this is the entity class defined for the repository. It may not be the class of the document that was read // we will reset it after reading the document @@ -79,18 +81,32 @@ public T decodeEntityBase(String id, String source, long cas, Class entit // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. // if this is a Collection or array, only the first element will be returned. + final CouchbaseDocument converted = new CouchbaseDocument(id); Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) .getContent().entrySet(); return (T) set.iterator().next().getValue(); } + if (id == null) { + throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " + + TemplateUtils.SELECT_ID); + } + + final CouchbaseDocument converted = new CouchbaseDocument(id); + // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure + // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure // can be avoid by defining the argument as Long instead of long. // persistentEntity is still the (possibly abstract) class specified in the repository definition // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); + if (persistentEntity.getVersionProperty() != null) { + if (cas == null) { + throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS + + " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS); + } + if (cas != 0) { + converted.put(persistentEntity.getVersionProperty().getName(), cas); + } } // if the constructor has an argument that is long version, then construction will fail if the 'version' @@ -101,13 +117,13 @@ public T decodeEntityBase(String id, String source, long cas, Class entit persistentEntity = couldBePersistentEntity(readEntity.getClass()); - if (cas != 0 && persistentEntity.getVersionProperty() != null) { + if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection); - if(holder != null){ - holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + if (holder != null) { + holder.transactionResultHolder(txResultHolder, (T) accessor.getBean()); } return accessor.getBean(); @@ -117,13 +133,16 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { return null; } - return mappingContext.getPersistentEntity(entityClass); - } - + try { + return mappingContext.getPersistentEntity(entityClass); + } catch (InaccessibleObjectException t) { + } + return null; + } public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() @@ -139,8 +158,8 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l accessor.setProperty(versionProperty, cas); } - if(holder != null){ - holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + if (holder != null) { + holder.transactionResultHolder(txResultHolder, (T) accessor.getBean()); } maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); return (T) accessor.getBean(); @@ -202,7 +221,7 @@ private boolean canPublishEvent() { return this.applicationContext != null; } - public TranslationService getTranslationService(){ + public TranslationService getTranslationService() { return translationService; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index f92fa6144..1169c98c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -35,6 +35,8 @@ import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.util.Assert; @@ -73,95 +75,13 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { } @Override - public T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection) { - - // this is the entity class defined for the repository. It may not be the class of the document that was read - // we will reset it after reading the document - // - // This will fail for the case where: - // 1) The version is defined in the concrete class, but not in the abstract class; and - // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in - // the source is null. - // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, - // but that is a lot of work to do every time just for this very rare and avoidable case. - // TypeInformation typeToUse = typeMapper.readType(source, type); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - final CouchbaseDocument converted = new CouchbaseDocument(id); - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - if (id == null) { - throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID); - } - - final CouchbaseDocument converted = new CouchbaseDocument(id); - - // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure - // can be avoid by defining the argument as Long instead of long. - // persistentEntity is still the (possibly abstract) class specified in the repository definition - // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (persistentEntity.getVersionProperty() != null) { - if (cas == null) { - throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS - + " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS); - } - if (cas != 0) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - } - - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - persistentEntity = couldBePersistentEntity(readEntity.getClass()); - - if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection); - return accessor.getBean(); - } - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - return mappingContext.getPersistentEntity(entityClass); - } - - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - try { - return mappingContext.getPersistentEntity(entityClass); - } catch (InaccessibleObjectException t) { - - } - return null; - } - - @Override - public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); } @Override - public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txHolder, CouchbaseResourceHolder holder) { return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @@ -225,4 +145,8 @@ protected T maybeCallAfterConvert(T object, CouchbaseDocument document, Stri return object; } + @Override + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template.reactive(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index 58e9146ea..2aed295fb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -37,8 +37,7 @@ * * @author Christoph Strobl * @since 2.0 - */ -public interface ExecutableFindByAnalyticsOperation { + */public interface ExecutableFindByAnalyticsOperation { /** * Queries the analytics service. @@ -115,23 +114,65 @@ default Optional first() { } + interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { + + /** + * Set the filter for the analytics query to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingFindByAnalytics matching(AnalyticsQuery query); + + } + /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * * @param options to use for execution */ @Override - TerminatingFindByAnalytics withOptions(AnalyticsOptions options); + FindByAnalyticsWithQuery withOptions(AnalyticsOptions options); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithOptions inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { + interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { /** * Allows to override the default scan consistency. @@ -139,7 +180,7 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions * @param scanConsistency the custom scan consistency to use for this analytics query. */ @Deprecated - FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); } @@ -153,22 +194,10 @@ interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWit FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } - interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - FindByAnalyticsWithConsistency matching(AnalyticsQuery query); - - } - /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { /** * Define the target type fields should be mapped to.
    @@ -178,39 +207,9 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithQuery as(Class returnType); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithProjection inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByAnalyticsInCollection inScope(String scope); + FindByAnalyticsWithConsistency as(Class returnType); } - interface ExecutableFindByAnalytics extends FindByAnalyticsInScope {} + interface ExecutableFindByAnalytics extends FindByAnalyticsWithProjection {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index b8284fe2a..1dcdf2dca 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -123,7 +123,7 @@ public FindByAnalyticsWithConsistency withConsistency(final AnalyticsScanCons } @Override - public FindByAnalyticsWithQuery as(final Class returnType) { + public FindByAnalyticsWithConsistency as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 00e43c2be..9cb60eeb7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -18,15 +18,14 @@ import java.time.Duration; import java.util.Collection; -import org.springframework.data.couchbase.core.support.InCollection; -import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; -import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; -import org.springframework.data.couchbase.core.support.WithTransaction; +import org.springframework.data.couchbase.core.support.InScope; import com.couchbase.client.java.kv.GetOptions; +import org.springframework.data.couchbase.core.support.WithExpiry; /** * Get Operations @@ -83,57 +82,19 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } - interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - @Override - FindByIdWithOptions project(String... fields); - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - - /** - * Provide attempt context - * - * @param the entity type to use for the results - */ - interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { - /** - * Finds the distinct values for a specified {@literal field} across a single collection - * - * @return new instance of {@link ExecutableFindById}. - * @throws IllegalArgumentException if field is {@literal null}. - */ - @Override - FindByIdWithProjection transaction(); - } - - interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { + interface FindByIdInCollection extends FindByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdTxOrNot inCollection(String collection); + FindByIdWithOptions inCollection(String collection); } /** @@ -151,11 +112,31 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } + interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + @Override + FindByIdInScope project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type to use for the results */ - interface ExecutableFindById extends FindByIdInScope {} + interface ExecutableFindById extends FindByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index a5cbd3f13..8d94a3a18 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -82,7 +82,7 @@ public TerminatingFindById withOptions(final GetOptions options) { } @Override - public FindByIdTxOrNot inCollection(final String collection) { + public FindByIdWithOptions inCollection(final String collection) { return new ExecutableFindByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, fields, expiry); } @@ -103,11 +103,6 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); } - @Override - public FindByIdWithExpiry transaction() { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 716476f0b..f147dda67 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -29,7 +29,6 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; @@ -130,100 +129,111 @@ default Optional first() { } /** - * Fluent method to specify options. + * Fluent methods to specify the query * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { + interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { + /** - * Fluent method to specify options to use for execution + * Set the filter for the query to be used. * - * @param options to use for execution + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ @Override - TerminatingFindByQuery withOptions(QueryOptions options); + TerminatingFindByQuery matching(Query query); + + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + @Override + default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** - * To be removed at the next major release. use WithConsistency instead + * Fluent method to specify options. * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryWithOptions { - + interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { /** - * Allows to override the default scan consistency. + * Fluent method to specify options to use for execution * - * @param scanConsistency the custom scan consistency to use for this query. + * @param options to use for execution */ - @Deprecated - FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + @Override + TerminatingFindByQuery withOptions(QueryOptions options); } /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { - + interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { /** - * Allows to override the default scan consistency. + * With a different collection * - * @param scanConsistency the custom scan consistency to use for this query. + * @param collection the collection to use. */ @Override - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + FindByQueryWithOptions inCollection(String collection); } /** - * Fluent method to specify transaction + * Fluent method to specify the scope. * * @param the entity type to use for the results. */ - interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { - + interface FindByQueryInScope extends FindByQueryInCollection, InScope { /** - * Finds the distinct values for a specified {@literal field} across a single collection + * With a different scope * - * @return new instance of {@link ExecutableFindByQuery}. - * @throws IllegalArgumentException if field is {@literal null}. + * @param scope the scope to use. */ @Override - TerminatingFindByQuery transaction(); + FindByQueryInCollection inScope(String scope); } - interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} - /** - * Fluent methods to specify the query - * + * To be removed at the next major release. use WithConsistency instead + * * @param the entity type to use for the results. */ - interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryInScope { /** - * Set the filter for the query to be used. + * Allows to override the default scan consistency. * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - @Override - FindByQueryTxOrNot matching(Query query); + @Deprecated + FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + } + + /** + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * + * @param the entity type to use for the results. + */ + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * Allows to override the default scan consistency. * - * @param criteria must not be {@literal null}. - * @return new instance of {@link ExecutableFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ @Override - default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** @@ -231,7 +241,7 @@ default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithQuery { + interface FindByQueryWithProjection extends FindByQueryWithConsistency { /** * Define the target type fields should be mapped to.
    @@ -241,7 +251,7 @@ interface FindByQueryWithProjection extends FindByQueryWithQuery { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithQuery as(Class returnType); + FindByQueryWithConsistency as(Class returnType); } /** @@ -277,37 +287,7 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByQueryWithProjecting distinct(String[] distinctFields); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByQueryWithDistinct inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByQueryInCollection inScope(String scope); + FindByQueryWithProjection distinct(String[] distinctFields); } /** @@ -315,6 +295,6 @@ interface FindByQueryInScope extends FindByQueryInCollection, InScope { * * @param the entity type to use for the results */ - interface ExecutableFindByQuery extends FindByQueryInScope {} + interface ExecutableFindByQuery extends FindByQueryWithDistinct {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index d5e0f8280..487caf476 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -96,7 +96,7 @@ public List all() { } @Override - public FindByQueryTxOrNot matching(final Query query) { + public TerminatingFindByQuery matching(final Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { scanCons = query.getScanConsistency(); @@ -121,7 +121,7 @@ public FindByQueryConsistentWith withConsistency(final QueryScanConsistency s } @Override - public FindByQueryWithQuery as(final Class returnType) { + public FindByQueryWithConsistency as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields); @@ -147,12 +147,6 @@ public FindByQueryWithProjecting distinct(final String[] distinctFields) { collection, options, dFields, fields); } - @Override - public FindByQueryWithDistinct transaction() { - return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); - } - @Override public Stream stream() { return reactiveSupport.all().toStream(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index ef4bb2295..0465d5022 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -24,7 +24,6 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; @@ -74,7 +73,8 @@ interface TerminatingInsertById extends OneAndAllEntity { * * @param the entity type to use. */ - interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertOptions { + interface InsertByIdWithOptions + extends TerminatingInsertById, WithInsertOptions { /** * Fluent method to specify options to use for execution. * @@ -84,42 +84,19 @@ interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertO TerminatingInsertById withOptions(InsertOptions options); } - interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { - - @Override - InsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - - interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { - @Override - InsertByIdWithExpiry transaction(); - } - - interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { + interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdTxOrNot inCollection(String collection); + InsertByIdWithOptions inCollection(String collection); } /** @@ -137,11 +114,27 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } + interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { + + @Override + InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ExecutableInsertById extends InsertByIdInScope {} + interface ExecutableInsertById extends InsertByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 5c86f7e1e..36193ae36 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -97,7 +97,7 @@ public InsertByIdInCollection inScope(final String scope) { } @Override - public InsertByIdTxOrNot inCollection(final String collection) { + public InsertByIdWithOptions inCollection(final String collection) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @@ -124,12 +124,6 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { durabilityLevel, expiry); } - @Override - public InsertByIdWithExpiry transaction() { - return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 48fb47d7e..47b5a8682 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -21,10 +21,8 @@ import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; -import org.springframework.data.couchbase.core.support.WithCas; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -89,7 +87,6 @@ interface TerminatingRemoveById extends OneAndAllId { List allEntities(Collection entities); } - /** * Fluent method to specify options. */ @@ -103,39 +100,17 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } - interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { - - @Override - RemoveByIdWithOptions withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { - @Override - RemoveByIdWithDurability withCas(Long cas); - } - - interface RemoveByIdWithTransaction extends TerminatingRemoveById, WithTransaction { - @Override - TerminatingRemoveById transaction(); - } - - interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} - /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { + interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - RemoveByIdTxOrNot inCollection(String collection); + RemoveByIdWithOptions inCollection(String collection); } /** @@ -151,9 +126,24 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } + interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { + + @Override + RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability { + + RemoveByIdWithDurability withCas(Long cas); + } + /** * Provides methods for constructing remove operations in a fluent way. */ - interface ExecutableRemoveById extends RemoveByIdInScope {} + interface ExecutableRemoveById extends RemoveByIdWithCas {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 710d79410..71ac65965 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -100,7 +100,7 @@ public List allEntities(final Collection entities) { @Override - public RemoveByIdTxOrNot inCollection(final String collection) { + public RemoveByIdWithOptions inCollection(final String collection) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, cas); } @@ -139,12 +139,6 @@ public RemoveByIdWithDurability withCas(Long cas) { durabilityLevel, cas); } - @Override - public RemoveByIdWithCas transaction() { - return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index d385e1a58..a6bfdf0cf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -21,9 +21,9 @@ import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -56,58 +56,32 @@ interface TerminatingRemoveByQuery { } /** - * Fluent method to specify options. + * Fluent methods to specify the query * - * @param the entity type to use for the results. + * @param the entity type. */ - interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { - /** - * Fluent method to specify options to use for execution - * - * @param options to use for execution - */ - TerminatingRemoveByQuery withOptions(QueryOptions options); - } + interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { - - @Deprecated - RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + TerminatingRemoveByQuery matching(Query query); - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith/*, WithConsistency */{ - //@Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** - * Fluent method to specify the transaction + * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { - @Override - TerminatingRemoveByQuery transaction(); - } - - interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} - - /** - * Fluent methods to specify the query - * - * @param the entity type. - */ - interface RemoveByQueryWithQuery extends RemoveByQueryWithTxOrNot, WithQuery { - - RemoveByQueryWithTxOrNot matching(Query query); - - default RemoveByQueryWithTxOrNot matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - + interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + RemoveByQueryWithQuery withOptions(QueryOptions options); } /** @@ -115,13 +89,13 @@ default RemoveByQueryWithTxOrNot matching(QueryCriteriaDefinition criteria) { * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithQuery inCollection(String collection); + RemoveByQueryWithOptions inCollection(String collection); } /** @@ -138,11 +112,25 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { + + @Deprecated + RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + @Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + + } + /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ExecutableRemoveByQuery extends RemoveByQueryInScope {} + interface ExecutableRemoveByQuery extends RemoveByQueryWithConsistency {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index dfd4f6102..7cab01cbd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -71,7 +71,7 @@ public List all() { } @Override - public RemoveByQueryWithTxOrNot matching(final Query query) { + public TerminatingRemoveByQuery matching(final Query query) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @@ -108,12 +108,6 @@ public RemoveByQueryInCollection inScope(final String scope) { scope != null ? scope : this.scope, collection, options); } - @Override - public TerminatingRemoveByQuery transaction() { - return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index 4dc60ed19..51ce8e98b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -24,7 +24,6 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -84,41 +83,19 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } - interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { - @Override - ReplaceByIdWithOptions withDurability(DurabilityLevel durabilityLevel); - - @Override - ReplaceByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - @Override - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - - interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { - // todo gp is this staying? It's confusing when doing ops.replaceById() inside @Transactional to get this transaction() method - unclear as a user whether I need to call it or not - @Override - TerminatingReplaceById transaction(); - } - - interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdTxOrNot inCollection(String collection); + ReplaceByIdWithOptions inCollection(String collection); } /** @@ -136,11 +113,24 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope { ReplaceByIdInCollection inScope(String scope); } + interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { + @Override + ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); + @Override + ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + @Override + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ExecutableReplaceById extends ReplaceByIdInScope {} + interface ExecutableReplaceById extends ReplaceByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 6c868588f..c68493d21 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -84,7 +84,7 @@ public Collection all(Collection objects) { } @Override - public ReplaceByIdTxOrNot inCollection(final String collection) { + public ReplaceByIdWithOptions inCollection(final String collection) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @@ -111,12 +111,6 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { replicateTo, durabilityLevel, expiry); } - @Override - public ReplaceByIdWithExpiry transaction() { - return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); - } - @Override public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index 56f93d02a..0831f8eb4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -83,32 +83,19 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } - interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { - @Override - UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithExpiry inCollection(String collection); + UpsertByIdWithOptions inCollection(String collection); } /** @@ -126,11 +113,25 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope { UpsertByIdInCollection inScope(String scope); } + interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { + @Override + UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ExecutableUpsertById extends UpsertByIdInScope {} + interface ExecutableUpsertById extends UpsertByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index f8769c743..2fe9ef891 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -42,13 +42,13 @@ public Mono encodeEntity(Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 2e5141e84..c2cd6d775 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -79,13 +79,13 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index 2d9f1251b..1d661b302 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -88,12 +88,24 @@ interface TerminatingFindByAnalytics extends OneAndAllReactive { } + interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { + + /** + * Set the filter for the analytics query to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingFindByAnalytics matching(AnalyticsQuery query); + + } + /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * @@ -103,47 +115,65 @@ interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, W TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } - @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { - + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { /** - * Allows to override the default scan consistency. + * With a different collection * - * @param scanConsistency the custom scan consistency to use for this analytics query. + * @param collection the collection to use. */ - @Deprecated - FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); + @Override + FindByAnalyticsWithOptions inCollection(String collection); + } + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { + @Deprecated + interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { /** * Allows to override the default scan consistency. * * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Override - FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); + @Deprecated + FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); } - interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { + interface FindByAnalyticsWithConsistency extends FindByAnalyticsInScope, WithAnalyticsConsistency { /** - * Set the filter for the analytics query to be used. + * Allows to override the default scan consistency. * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this analytics query. */ - FindByAnalyticsWithConsistency matching(AnalyticsQuery query); + @Override + FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency); } /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { /** * Define the target type fields should be mapped to.
    @@ -153,39 +183,9 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithQuery as(Class returnType); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithProjection inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByAnalyticsInCollection inScope(String scope); + FindByAnalyticsWithConsistency as(Class returnType); } - interface ReactiveFindByAnalytics extends FindByAnalyticsInScope {} + interface ReactiveFindByAnalytics extends FindByAnalyticsWithProjection, FindByAnalyticsConsistentWith {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index 10f84863c..05ba33b22 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -90,7 +90,7 @@ public FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scan } @Override - public FindByAnalyticsWithQuery as(final Class returnType) { + public FindByAnalyticsWithConsistency as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 16001f177..5e9983b0f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -27,12 +27,11 @@ import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; /** - * Get Operations - method/interface chaining is from the bottom up. + * Get Operations * * @author Christoph Strobl * @since 2.0 @@ -68,20 +67,7 @@ interface TerminatingFindById extends OneAndAllIdReactive { * @return the list of found entities. */ Flux all(Collection ids); - } - /** - * Provide transaction - * - * @param the entity type to use for the results - */ - interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { - /** - * Provide transaction - * - * @return - */ - TerminatingFindById transaction(); } /** @@ -99,45 +85,19 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } - interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - FindByIdWithOptions project(String... fields); - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - - /** - * Interface to that can produce either transactional or non-transactional operations. - * - * @param the entity type to use for the results. - */ - interface FindByIdTxOrNot extends FindByIdWithTransaction, FindByIdWithExpiry {} - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { + interface FindByIdInCollection extends FindByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdTxOrNot inCollection(String collection); + FindByIdWithOptions inCollection(String collection); } /** @@ -155,11 +115,32 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } + interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { + + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + FindByIdInCollection project(String... fields); + + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + /** * Provides methods for constructing query operations in a fluent way. * - * @param the entity type. + * @param the entity type to use for the results */ - interface ReactiveFindById extends FindByIdInScope {}; + interface ReactiveFindById extends FindByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 156182dbe..1151a9a4e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -154,7 +154,7 @@ public FindByIdInCollection inScope(final String scope) { } @Override - public FindByIdWithOptions project(String... fields) { + public FindByIdInCollection project(String... fields) { Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), expiry, @@ -167,12 +167,6 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { support); } - @Override - public FindByIdWithProjection transaction() { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, - support); - } - private CommonOptions initGetOptions() { CommonOptions getOptions; if (expiry != null || options instanceof GetAndTouchOptions) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index f55d65da5..9a839ed7a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -28,7 +28,6 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -93,98 +92,94 @@ interface TerminatingFindByQuery extends OneAndAllReactive { } /** - * Fluent method to specify options. + * Fluent methods to filter by query * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { + interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { + /** - * @param options options to use for execution + * Set the filter {@link Query} to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - TerminatingFindByQuery withOptions(QueryOptions options); - } - - /** - * To be removed at the next major release. use WithConsistency instead - * - * @param the entity type to use for the results. - */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryWithOptions { + TerminatingFindByQuery matching(Query query); /** - * Allows to override the default scan consistency. + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param criteria must not be {@literal null}. + * @return new instance of {@link TerminatingFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. */ - @Deprecated - FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. - * + * Fluent method to specify options. + * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { - + interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { /** - * Allows to override the default scan consistency. - * - * @param scanConsistency the custom scan consistency to use for this query. + * @param options options to use for execution */ - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - + TerminatingFindByQuery withOptions(QueryOptions options); } /** - * Fluent method to add transactions - * + * Fluent method to specify the collection + * * @param the entity type to use for the results. */ - interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { - - /** - * Finds the distinct values for a specified {@literal field} across a single {@link } or view. - * - * @return new instance of {@link ReactiveFindByQuery}. - * @throws IllegalArgumentException if field is {@literal null}. - */ - TerminatingFindByQuery transaction(); + interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { + FindByQueryWithOptions inCollection(String collection); } /** - * Fluent interface for operations with or without a transaction. + * Fluent method to specify the scope * * @param the entity type to use for the results. */ - interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + FindByQueryInCollection inScope(String scope); + } /** - * Fluent methods to filter by query + * To be removed at the next major release. use WithConsistency instead * * @param the entity type to use for the results. */ - interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryInScope { /** - * Set the filter {@link Query} to be used. + * Allows to override the default scan consistency. * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - FindByQueryTxOrNot matching(Query query); + @Deprecated + FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + + } + + /** + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * + * @param the entity type to use for the results. + */ + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * Allows to override the default scan consistency. * - * @param criteria must not be {@literal null}. - * @return new instance of {@link TerminatingFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } @@ -193,7 +188,7 @@ default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithQuery { + interface FindByQueryWithProjection extends FindByQueryWithConsistency { /** * Define the target type fields should be mapped to.
    @@ -203,7 +198,7 @@ interface FindByQueryWithProjection extends FindByQueryWithQuery { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithQuery as(Class returnType); + FindByQueryWithConsistency as(Class returnType); } /** @@ -238,25 +233,7 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - FindByQueryWithProjecting distinct(String[] distinctFields); - } - - /** - * Fluent method to specify the collection - * - * @param the entity type to use for the results. - */ - interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { - FindByQueryWithDistinct inCollection(String collection); - } - - /** - * Fluent method to specify the scope - * - * @param the entity type to use for the results. - */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { - FindByQueryInCollection inScope(String scope); + FindByQueryWithProjection distinct(String[] distinctFields); } /** @@ -264,6 +241,6 @@ interface FindByQueryInScope extends FindByQueryInCollection, InScope { * * @param the entity type to use for the results */ - interface ReactiveFindByQuery extends FindByQueryInScope {} + interface ReactiveFindByQuery extends FindByQueryWithDistinct {} } 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 f18be74bd..481802454 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -27,7 +27,6 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; -import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -56,7 +55,7 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, - OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, null, null, + OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, null, null, template.support()); } @@ -129,7 +128,7 @@ public FindByQueryWithDistinct inCollection(final String collection) { @Override @Deprecated - public FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency) { + public FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields, support); @@ -171,13 +170,6 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { support); } - @Override - public FindByQueryWithTransaction transaction() { - return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); - } - @Override public Mono one() { return all().singleOrEmpty(); @@ -192,7 +184,7 @@ public Mono first() { public Flux all() { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); - String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); + String statement = assembleEntityQuery(false, distinctFields, pArgs.getScope(), pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); @@ -219,7 +211,7 @@ public Flux all() { }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())).flatMap(row -> { String id = ""; - long cas = 0; + Long cas = Long.valueOf(0); if (!query.isDistinct() && distinctFields == null) { id = row.getString(TemplateUtils.SELECT_ID); if (id == null) { @@ -232,7 +224,7 @@ public Flux all() { row.removeKey(TemplateUtils.SELECT_CAS_3x); } row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS) + row.removeKey(TemplateUtils.SELECT_CAS); } System.err.println("row: "+row); return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), @@ -254,7 +246,7 @@ private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { public Mono count() { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); - String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); + String statement = assembleEntityQuery(true, distinctFields, pArgs.getScope(), pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 70cba57ff..6439879fe 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -27,7 +27,6 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; @@ -85,43 +84,17 @@ interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertO TerminatingInsertById withOptions(InsertOptions options); } - interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { - - @Override - InsertByIdInCollection withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - - interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { - @Override - InsertByIdWithDurability transaction(); - } - /** * Fluent method to specify the collection. */ - interface InsertByIdTxOrNot extends InsertByIdWithTransaction, InsertByIdWithExpiry {} - - /** - * Fluent method to specify the collection. - */ - interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { + interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdTxOrNot inCollection(String collection); + InsertByIdWithOptions inCollection(String collection); } /** @@ -137,11 +110,27 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } + interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { + + @Override + InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ReactiveInsertById extends InsertByIdInScope {} + interface ReactiveInsertById extends InsertByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index f56731c8a..b6d549b60 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,7 +15,6 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.transaction.CoreTransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -153,7 +152,7 @@ public InsertByIdInCollection inScope(final String scope) { } @Override - public InsertByIdTxOrNot inCollection(final String collection) { + public InsertByIdWithOptions inCollection(final String collection) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); @@ -184,13 +183,6 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { support); } - @Override - public InsertByIdWithExpiry transaction() { - return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index 42a6211c9..a8d5cc8cc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -25,7 +25,6 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -101,42 +100,22 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } - interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { - @Override - RemoveByIdInCollection withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - - interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { - RemoveByIdWithCas transaction(); - } - - interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} - /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { + interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByIdTxOrNot inCollection(String collection); + RemoveByIdWithOptions inCollection(String collection); } /** * Fluent method to specify the scope. */ - interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { + interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { /** * With a different scope * @@ -145,9 +124,23 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { + @Override + RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability { + + RemoveByIdWithDurability withCas(Long cas); + } + /** * Provides methods for constructing remove operations in a fluent way. */ - interface ReactiveRemoveById extends RemoveByIdInScope {}; + interface ReactiveRemoveById extends RemoveByIdWithCas {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index aa20118b4..252881612 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -195,12 +195,6 @@ public RemoveByIdWithDurability withCas(Long cas) { durabilityLevel, cas); } - @Override - public RemoveByIdWithCas transaction() { - return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 3388dd648..7619eabf1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -24,7 +24,6 @@ import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -55,61 +54,31 @@ interface TerminatingRemoveByQuery { } /** - * Fluent method to specify options. + * Fluent methods to specify the query * - * @param the entity type to use for the results. + * @param the entity type. */ - interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { - /** - * Fluent method to specify options to use for execution - * - * @param options to use for execution - */ - TerminatingRemoveByQuery withOptions(QueryOptions options); - } + interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { - - @Deprecated - RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + TerminatingRemoveByQuery matching(Query query); + default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** - * Fluent method to specify the transaction + * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { + interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { /** - * Provide the transaction + * Fluent method to specify options to use for execution * + * @param options to use for execution */ - @Override - TerminatingRemoveByQuery transaction(); - } - - interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} - - /** - * Fluent methods to specify the query - * - * @param the entity type. - */ - interface RemoveByQueryWithQuery extends RemoveByQueryTxOrNot, WithQuery { - - RemoveByQueryTxOrNot matching(Query query); - - default RemoveByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } + RemoveByQueryWithQuery withOptions(QueryOptions options); } /** @@ -117,13 +86,13 @@ default RemoveByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithQuery inCollection(String collection); + RemoveByQueryWithOptions inCollection(String collection); } /** @@ -140,11 +109,25 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { + + @Deprecated + RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + @Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + + } + /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ReactiveRemoveByQuery extends RemoveByQueryInScope {} + interface ReactiveRemoveByQuery extends RemoveByQueryWithConsistency {} } 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 e11ac7b8d..2b4b63a7b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -26,7 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; @@ -111,13 +110,13 @@ private QueryOptions buildQueryOptions(QueryOptions options) { } @Override - public RemoveByQueryTxOrNot matching(final Query query) { + public TerminatingRemoveByQuery matching(final Query query) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @Override - public RemoveByQueryWithQuery inCollection(final String collection) { + public RemoveByQueryWithOptions inCollection(final String collection) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection != null ? collection : this.collection, options); } @@ -153,12 +152,6 @@ public RemoveByQueryInCollection inScope(final String scope) { scope != null ? scope : this.scope, collection, options); } - @Override - public RemoveByQueryWithConsistency transaction() { - return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index d78f40269..0341096cb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -27,7 +27,6 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; -import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -85,39 +84,19 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } - interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { - - ReplaceByIdInCollection withDurability(DurabilityLevel durabilityLevel); - - ReplaceByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - - interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { - @Override - ReplaceByIdWithExpiry transaction(); - } - - interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdTxOrNot inCollection(String collection); + ReplaceByIdWithOptions inCollection(String collection); } /** @@ -135,11 +114,24 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope inScope(String scope); } + interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { + + ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); + + ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ReactiveReplaceById extends ReplaceByIdInScope {}; + interface ReactiveReplaceById extends ReplaceByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 00e76110d..17c38d454 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -199,13 +199,6 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { support); } - @Override - public ReplaceByIdWithExpiry transaction() { - return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); - } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 770df7f73..cd5fd8c23 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -29,10 +29,10 @@ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index b3045a25b..05249e198 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -86,32 +86,19 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } - interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { - @Override - UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithExpiry inCollection(String collection); + UpsertByIdWithOptions inCollection(String collection); } /** @@ -129,11 +116,25 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope inScope(String scope); } + interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { + @Override + UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ReactiveUpsertById extends UpsertByIdInScope {} + interface ReactiveUpsertById extends UpsertByIdWithExpiry {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index df50cd41b..0192ea7fb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -29,9 +29,9 @@ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); + T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); + T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index 31775fb2d..40ee839b5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -55,15 +55,6 @@ public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } - /** - * Returns the repository interface - * - * @return the underlying entity information. - */ - public Class getRepositoryInterface() { - return repositoryInterface; - } - Class getJavaType() { return getEntityInformation().getJavaType(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 1469e6ee5..778924fe2 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -202,7 +202,7 @@ public Mono delete(T entity) { private Mono delete(T entity, String scope, String collection) { Assert.notNull(entity, "Entity must not be null!"); - return operations.removeById(getJavaType()).inScope(scope).inCollection(collection).one(getId(entity)).then(); + return operations.removeById(getJavaType()).inScope(scope).inCollection(collection).oneEntity(entity).then(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java index 5d65acab6..9165d88ca 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java @@ -18,7 +18,7 @@ import org.springframework.dao.UncategorizedDataAccessException; import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; -import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException; +//import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException; /** * An opaque signal that something went wrong during the execution of an operation inside a transaction. @@ -28,7 +28,7 @@ * Internal state has been set that ensures that the transaction will act appropriately (including rolling * back and retrying if necessary) regardless of what the application does with this exception. */ -public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException implements WrappedTransactionOperationFailedException { +public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException implements WrappedTransactionOperationFailedException { private final TransactionOperationFailedException internal; public UncategorizedTransactionDataAccessException(TransactionOperationFailedException err) { diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index 8543ed78e..3d814ac37 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -26,6 +26,8 @@ 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.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 1dedbad5c..7be304ea4 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -111,16 +111,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).all(); } @AfterEach @@ -129,7 +129,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); // then call the super method super.afterEach(); } @@ -142,8 +142,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -185,8 +185,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } @@ -210,8 +210,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) - .withConsistency(REQUEST_PLUS).all(); + .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(daveUsers) + .all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -227,12 +227,12 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(specialUsers).all(); assertEquals(1, foundUsers.size()); - final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).matching(specialUsers) .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); @@ -252,8 +252,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); + List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -277,8 +277,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) - .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(nonSpecialUsers) + .all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -301,18 +301,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -320,8 +320,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) - .as(icaoClass).withConsistency(REQUEST_PLUS).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count2); } finally { @@ -345,20 +345,20 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all().collectList() .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class) .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .count().block(); assertEquals(2, count1); // count( distinct (all fields in icaoClass) // which only has one field @@ -366,8 +366,8 @@ void distinctReactive() { Class icaoClass = (new Object() { String icao; }).getClass(); - long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).count().block(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .count().block(); assertEquals(7, count2); } finally { @@ -440,8 +440,8 @@ public void findByQuery() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("441")); try { - List found = couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()); @@ -492,9 +492,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("495")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName) - .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .withConsistency(REQUEST_PLUS).withOptions(options).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -583,8 +583,8 @@ public void findByQueryOther() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("594")); try { - List found = couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) - .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()); @@ -635,9 +635,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("648")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope) - .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .withConsistency(REQUEST_PLUS).withOptions(options).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -700,8 +700,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) - .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all()); + assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).all()); } @Test @@ -739,9 +739,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) - .withOptions(options).all()); + () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) + .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all()); } @Test @@ -765,8 +765,8 @@ public void testScopeCollectionAnnotation() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) - .matching(query).withConsistency(REQUEST_PLUS).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .matching(query).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); @@ -785,8 +785,8 @@ public void testScopeCollectionRepoWith() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) - .matching(query).withConsistency(REQUEST_PLUS).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .matching(query).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); @@ -804,7 +804,7 @@ void testFluentApi() { DurabilityLevel dl = DurabilityLevel.NONE; User result; RemoveResult rr; - result = couchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) + result = couchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) .one(user1); assertEquals(user1, result); result = couchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 691f0dc8a..b1f15371a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -137,8 +137,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).matching(specialUsers) - .withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).matching(specialUsers) + .all(); assertEquals(1, foundUsers.size()); } @@ -151,8 +151,8 @@ void findAssessmentDO() { ado = couchbaseTemplate.upsertById(AssessmentDO.class).one(ado); Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); - final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) - .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class).withConsistency(REQUEST_PLUS) + .matching(specialUsers).all(); assertEquals("123", foundUsers.get(0).getId(), "id"); assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); assertEquals(ado, foundUsers.get(0)); @@ -180,7 +180,7 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).matching(daveUsers).withConsistency(REQUEST_PLUS).all(); + .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).matching(daveUsers).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -197,11 +197,11 @@ void findByMatchingQueryProjected() { Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all().collectList().block(); + .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).matching(specialUsers).all().collectList().block(); assertEquals(1, foundUsersReactive.size()); couchbaseTemplate.removeById(User.class).all(Arrays.asList(user1.getId(), user2.getId(), specialUser.getId())); @@ -238,7 +238,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where(i("firstname")).notLike("special")); - couchbaseTemplate.removeByQuery(User.class).matching(nonSpecialUsers).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).matching(nonSpecialUsers).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); @@ -346,7 +346,7 @@ void sortedTemplate() { .query(QueryCriteria.where("iata").isNotNull()); Pageable pageableWithSort = PageRequest.of(0, 7, Sort.by("iata")); query.with(pageableWithSort); - List airports = couchbaseTemplate.findByQuery(Airport.class).matching(query).withConsistency(REQUEST_PLUS) + List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).matching(query) .all(); String[] sortedIatas = iatas.clone(); diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 26dafb8a0..f4b7fe5ca 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -110,14 +110,14 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .all(); couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) + .all(); template = reactiveCouchbaseTemplate; } @@ -128,7 +128,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); // then call the super method super.afterEach(); } @@ -141,8 +141,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -184,8 +184,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } @@ -209,8 +209,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) - .withConsistency(REQUEST_PLUS).all(); + .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(daveUsers) + .all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -226,12 +226,12 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) - .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(specialUsers).all(); assertEquals(1, foundUsers.size()); - final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).matching(specialUsers) .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); @@ -248,8 +248,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).all(); + List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -273,8 +273,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) - .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(nonSpecialUsers) + .all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -297,18 +297,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -316,8 +316,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) - .as(icaoClass).withConsistency(REQUEST_PLUS).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count2); } finally { @@ -341,25 +341,25 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all().collectList() .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) - .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .count().block(); assertEquals(2, count1); // count( distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] { "iata", "icao" }) - .withConsistency(REQUEST_PLUS).count().block(); + Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block(); assertEquals(7, count2); } finally { @@ -432,8 +432,8 @@ public void findByQuery() { // 4 Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowa")).block(); try { - List found = template.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) - .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()).block(); @@ -484,9 +484,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowe")).block(); - List removeResults = template.removeByQuery(Airport.class).inScope(scopeName) - .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); + List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -574,8 +574,8 @@ public void findByQueryOther() { // 4 Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowj")).block(); try { - List found = template.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) + .withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); @@ -626,9 +626,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lown")).block(); - List removeResults = template.removeByQuery(Airport.class).inScope(otherScope) - .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); + List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -691,8 +691,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).inScope(otherScope) - .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block()); + assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).all().collectList().block()); } @Test @@ -730,9 +730,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> template.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) - .withOptions(options).all().collectList().block()); + () -> template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) + .withOptions(options) .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .all().collectList().block()); } @Test 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 39daea436..124584d73 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -16,9 +16,8 @@ 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 com.couchbase.client.java.query.QueryOptions.queryOptions; +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; @@ -110,9 +109,8 @@ import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.InsertOptions; -import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.kv.MutationState; -import com.couchbase.client.java.kv.UpsertOptions; +import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -360,6 +358,9 @@ public void saveNotBoundedRequestPlus() { @Test public void saveNotBoundedWithDefaultRepository() { + if (!config().isUsingCloud()) { + return; + } airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll(); ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); // the Config class has been modified, these need to be loaded again @@ -370,13 +371,19 @@ public void saveNotBoundedWithDefaultRepository() { List sizeBeforeTest = airportRepositoryRP.findAll(); assertEquals(0, sizeBeforeTest.size()); - Airport vie = new Airport("airports::vie", "vie", "low9"); - Airport saved = airportRepositoryRP.save(vie); - List allSaved = airportRepositoryRP.findAll(); - couchbaseTemplate.removeById(Airport.class).one(saved.getId()); - if (!config().isUsingCloud()) { - assertTrue(allSaved.isEmpty(), "should not have been empty"); + boolean notFound = false; + for (int i = 0; i < 100; i++) { + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport saved = airportRepositoryRP.save(vie); + List allSaved = airportRepositoryRP.findAll(); + couchbaseTemplate.removeById(Airport.class).one(saved.getId()); + System.err.println(i); + if (allSaved.isEmpty()) { + notFound = true; + break; + } } + assertTrue(notFound, "the doc should not have been found. maybe"); } @Test @@ -403,11 +410,9 @@ void findByTypeAlias() { try { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); - List airports = couchbaseTemplate.findByQuery(Airport.class) + List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) .matching(org.springframework.data.couchbase.core.query.Query .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) - .withConsistency(REQUEST_PLUS) - .all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { @@ -891,7 +896,7 @@ void threadSafeStringParametersTest() throws Exception { } @Test - // DATACOUCH-650 + // DATACOUCH-650 void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); @@ -910,8 +915,8 @@ void deleteAllById() { void couchbaseRepositoryQuery() throws Exception { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); - couchbaseTemplate.findByQuery(User.class).matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")) - .withConsistency(REQUEST_PLUS).all(); + 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); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index bbeb31b49..f46e9c01e 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.auditing.DateTimeProvider; @@ -59,10 +60,8 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareIntegrationTests { @Autowired ReactiveUserRepository userRepository; - @Autowired ReactiveAirportRepository reactiveAirportRepository; - - @Autowired ReactiveAirlineRepository airlineRepository; + @Autowired ReactiveAirlineRepository reactiveAirlineRepository; @Test @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @@ -82,35 +81,11 @@ void saveReplaceUpsertInsert() { // Airline does not have a version Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null); // save the document - we don't care how on this call - airlineRepository.save(airline).block(); - airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. - airlineRepository.delete(airline).block(); + reactiveAirlineRepository.save(airline).block(); + reactiveAirlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. + reactiveAirlineRepository.delete(airline).block(); } - @Autowired ReactiveAirlineRepository airlineRepository; - - @Test - @IgnoreWhen(clusterTypes = ClusterType.MOCKED) - void saveReplaceUpsertInsert() { - // the User class has a version. - User user = new User(UUID.randomUUID().toString(), "f", "l"); - // save the document - we don't care how on this call - userRepository.save(user).block(); - // Now set the version to 0, it should attempt an insert and fail. - long saveVersion = user.getVersion(); - user.setVersion(0); - assertThrows(DuplicateKeyException.class, () -> userRepository.save(user).block()); - user.setVersion(saveVersion + 1); - assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user).block()); - userRepository.delete(user); - - // Airline does not have a version - Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline"); - // save the document - we don't care how on this call - airlineRepository.save(airline).block(); - airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. - airlineRepository.delete(airline).block(); - } @Test void saveAndFindById() { diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index f3161ff68..4809e221c 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -102,19 +102,19 @@ void testQuery() { Airport jfk = null; try { vie = new Airport("airports::vie", "vie", "low1"); - airportRepository.save(vie).block(); + reactiveAirportRepository.save(vie).block(); jfk = new Airport("airports::jfk", "JFK", "xxxx"); - airportRepository.save(jfk).block(); + reactiveAirportRepository.save(jfk).block(); - List all = airportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); + List all = reactiveAirportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); System.out.println(all); assertFalse(all.isEmpty()); assertTrue(all.stream().anyMatch(a -> a.equals("airports::vie"))); assertTrue(all.stream().anyMatch(a -> a.equals("airports::jfk"))); } finally { - airportRepository.delete(vie).block(); - airportRepository.delete(jfk).block(); + reactiveAirportRepository.delete(vie).block(); + reactiveAirportRepository.delete(jfk).block(); } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index e7d962346..0b4cb0e19 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -64,7 +64,6 @@ * * @author Michael Reiche */ -@SpringJUnitConfig(Config.class) @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(CollectionsConfig.class) public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @@ -236,7 +235,7 @@ public void testScopeCollectionAnnotationSwap() { assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); } finally { List removed = couchbaseTemplate.removeByQuery(Airport.class).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all(); } } @@ -291,7 +290,7 @@ void findPlusN1qlJoinBothAnnotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName) .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. @@ -346,7 +345,7 @@ void findPlusN1qlJoinUnannotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName) .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 3aac657f8..003f5b71c 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -55,9 +55,8 @@ * * @author Michael Reiche */ -@SpringJUnitConfig(Config.class) -@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(CollectionsConfig.class) +@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired ReactiveAirportRepository reactiveAirportRepository; diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java deleted file mode 100644 index 516f4c9ca..000000000 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2017-2022 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.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; - -import java.lang.reflect.Method; -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; -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.query.Query; -import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.domain.UserRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; -import org.springframework.data.repository.query.DefaultParameters; -import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; - -/** - * @author Michael Nitschinger - * @author Michael Reiche - */ -class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests { - - MappingContext, CouchbasePersistentProperty> context; - CouchbaseConverter converter; - CouchbaseTemplate couchbaseTemplate; - static NamedQueries namedQueries = new PropertiesBasedNamedQueries(new Properties()); - - @BeforeEach - public void beforeEach() { - context = new CouchbaseMappingContext(); - converter = new MappingCouchbaseConverter(context); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - } - - @Test - void createsQueryCorrectly() throws Exception { - String input = "getByFirstnameAndLastname"; - Method method = UserRepository.class.getMethod(input, String.class, String.class); - - CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, - new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), - converter.getMappingContext()); - - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); - - Query query = creator.createQuery(); - assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2", - query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); - } - - @Test - void createsQueryCorrectly2() throws Exception { - String input = "getByFirstnameOrLastname"; - Method method = UserRepository.class.getMethod(input, String.class, String.class); - - CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, - new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), - converter.getMappingContext()); - - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); - - Query query = creator.createQuery(); - assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)", - query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); - } - - @Test - void wrongNumberArgs() throws Exception { - String input = "getByFirstnameOrLastname"; - Method method = UserRepository.class.getMethod(input, String.class, String.class); - - CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, - new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), - converter.getMappingContext()); - - try { - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); - } catch (IllegalArgumentException e) { - return; - } - fail("should have failed with IllegalArgumentException: Invalid number of parameters given!"); - } - - @Test - void doesNotHaveAnnotation() throws Exception { - String input = "findByFirstname"; - Method method = UserRepository.class.getMethod(input, String.class); - CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, - new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), - converter.getMappingContext()); - - try { - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); - } catch (IllegalArgumentException e) { - return; - } - fail("should have failed with IllegalArgumentException: query has no inline Query or named Query not found"); - } - - private ParameterAccessor getAccessor(Parameters params, Object... values) { - return new ParametersParameterAccessor(params, values); - } - - private Parameters getParameters(Method method) { - return new DefaultParameters(method); - } - - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - static class Config 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(); - } - - } -} diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index cfd4ebb89..a29d532c2 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -29,6 +29,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.mapping.context.MappingContext; @@ -71,31 +72,11 @@ void wrongNumberArgs() throws Exception { converter.getMappingContext()); try { - Airline modified = couchbaseTemplate.upsertById(Airline.class).one(airline); - - String input = "getByName"; - Method method = AirlineRepository.class.getMethod(input, String.class); - - CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, - new DefaultRepositoryMetadata(AirlineRepository.class), new SpelAwareProxyProjectionFactory(), - converter.getMappingContext()); - - StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), - queryMethod, converter, config().bucketname(), new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); - - Query query = creator.createQuery(); - - ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class).matching(query) - .withConsistency(QueryScanConsistency.REQUEST_PLUS); - - Optional al = q.one(); - assertEquals(airline.toString(), al.get().toString()); - } catch (Exception e) { - e.printStackTrace(); - throw e; - } finally { - couchbaseTemplate.removeById().one(airline.getId()); + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), + queryMethod, converter, new SpelExpressionParser(), QueryMethodEvaluationContextProvider.DEFAULT, + namedQueries); + } catch (IllegalArgumentException e) { + return; } fail("should have failed with IllegalArgumentException: Invalid number of parameters given!"); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 00e05002b..d1127fc22 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -105,18 +105,18 @@ public void beforeEachTest() { WalterWhite = new Person("Walter", "White"); TransactionTestUtil.assertNotInTransaction(); List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = cbTmpl.removeByQuery(Person.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); List rp2 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); - List rp3 = cbTmpl.removeByQuery(EventLog.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List rp3 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List p1 = cbTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).withConsistency(REQUEST_PLUS) + List p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) .all(); List e0 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); - List e1 = cbTmpl.findByQuery(EventLog.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List e1 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java index 8610c9a15..4f38849e3 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -89,10 +89,10 @@ public void beforeEachTest() { TransactionTestUtil.assertNotInTransaction(); TransactionTestUtil.assertNotInTransaction(); List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = cbTmpl.removeByQuery(Person.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List p1 = cbTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).withConsistency(REQUEST_PLUS) + List p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) .all(); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java index ae10994ba..2a9b01737 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.transactions; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -194,8 +195,8 @@ public void committedRemoveByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doMonoInTransaction(() -> { - return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.id())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().next(); + return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(person.id())) + .all().next(); }); Person fetched = blocking.findById(Person.class).one(person.id()); @@ -209,8 +210,8 @@ public void committedFindByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doMonoInTransaction(() -> { - return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.id())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().next(); + return ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(person.id())) + .all().next(); }); assertEquals(1, rr.attempts); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java index 2f56e4e49..bffca926b 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java @@ -29,7 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -44,6 +43,7 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonWithoutVersion; import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -53,21 +53,17 @@ import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.support.TransactionTemplate; -import com.couchbase.client.java.transactions.error.TransactionFailedException; - /** - * Tests for Spring's TransactionTemplate, used CouchbaseCallbackTransactionManager, using template methods - * (findById etc.) + * Tests for Spring's TransactionTemplate, used CouchbaseCallbackTransactionManager, using template methods (findById + * etc.) */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) public class TransactionTemplateIntegrationTests extends JavaIntegrationTests { TransactionTemplate template; - @Autowired - CouchbaseCallbackTransactionManager transactionManager; + @Autowired CouchbaseCallbackTransactionManager transactionManager; @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired CouchbaseTemplate ops; Person WalterWhite; @@ -172,9 +168,8 @@ public void committedRemoveByQuery() { Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doInTransaction(status -> { - List removed = ops.removeByQuery(Person.class) - .matching(QueryCriteria.where("firstname").eq(person.getFirstname())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + List removed = ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); assertEquals(1, removed.size()); }); @@ -189,9 +184,8 @@ public void committedFindByQuery() { Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doInTransaction(status -> { - List found = ops.findByQuery(Person.class) - .matching(QueryCriteria.where("firstname").eq(person.getFirstname())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + List found = ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); assertEquals(1, found.size()); }); @@ -259,8 +253,8 @@ public void rollbackRemoveByQuery() { assertThrowsWithCause(() -> doInTransaction(status -> { attempts.incrementAndGet(); - ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); throw new SimulateFailureException(); }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); @@ -277,8 +271,8 @@ public void rollbackFindByQuery() { assertThrowsWithCause(() -> doInTransaction(status -> { attempts.incrementAndGet(); - ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all(); throw new SimulateFailureException(); }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java index 0dbad3aba..c30ee137c 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java @@ -93,12 +93,12 @@ public void beforeEachTest() { WalterWhite = new Person( "Walter", "White"); TransactionTestUtil.assertNotInTransaction(); List rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = operations.removeByQuery(Person.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List rp1 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); List p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List p1 = operations.findByQuery(Person.class).inScope(sName).inCollection(cName) - .withConsistency(REQUEST_PLUS).all(); + List p1 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .all(); } @AfterEach @@ -210,8 +210,8 @@ public void findPersonCBTransactions() { List docs = new LinkedList<>(); Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - return rxCBTmpl.findByQuery(Person.class).inScope(sName).inCollection(cName).matching(q) - .withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> { + return rxCBTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName).matching(q) + .one().doOnSuccess(doc -> { System.err.println("doc: " + doc); docs.add(doc); }); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java index 9b5563b38..2a650858d 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.transactions.sdk; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -164,8 +165,8 @@ public void committedRemoveByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doInTransaction(ctx -> { - return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.id())) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).all().then(); + return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(WalterWhite.id())) + .all().then(); }); Person fetched = blocking.findById(Person.class).one(person.id()); diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index aa39572bb..d294fd368 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -53,6 +53,8 @@ import org.opentest4j.AssertionFailedError; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; @@ -105,6 +107,7 @@ public class JavaIntegrationTests extends ClusterAwareIntegrationTests { @BeforeAll public static void beforeAll() { + Config.setScopeName(null); callSuperBeforeAll(new Object() {}); ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); From c6a1adea1646f6c0fe4ac912f41e71e3c977e02b Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Mon, 27 Jun 2022 18:29:23 -0700 Subject: [PATCH 19/19] Rebased and tidied transaction branch. Documentation to follow. Closes #1145. --- Jenkinsfile | 2 +- pom.xml | 11 -- .../AttemptContextReactiveAccessor.java | 4 +- .../couchbase/CouchbaseClientFactory.java | 11 +- .../SimpleCouchbaseClientFactory.java | 22 +-- .../AbstractCouchbaseConfiguration.java | 38 ++---- .../data/couchbase/config/BeanNames.java | 4 +- .../core/AbstractTemplateSupport.java | 8 +- .../core/CouchbaseExceptionTranslator.java | 32 ++++- .../couchbase/core/CouchbaseOperations.java | 10 +- .../couchbase/core/CouchbaseTemplate.java | 39 ++---- .../core/CouchbaseTemplateSupport.java | 31 +---- ...utableFindByAnalyticsOperationSupport.java | 5 + .../ExecutableFindByIdOperationSupport.java | 18 ++- .../core/ExecutableFindByQueryOperation.java | 2 +- ...ExecutableFindByQueryOperationSupport.java | 4 +- .../ExecutableInsertByIdOperationSupport.java | 12 +- .../core/ExecutableRemoveByIdOperation.java | 4 +- .../ExecutableRemoveByIdOperationSupport.java | 24 ++-- ...ecutableRemoveByQueryOperationSupport.java | 7 +- ...ExecutableReplaceByIdOperationSupport.java | 12 +- .../core/NonReactiveSupportWrapper.java | 17 +-- .../core/ReactiveCouchbaseOperations.java | 17 +-- .../core/ReactiveCouchbaseTemplate.java | 76 ++--------- .../ReactiveCouchbaseTemplateSupport.java | 40 ++---- .../ReactiveExistsByIdOperationSupport.java | 16 ++- ...activeFindByAnalyticsOperationSupport.java | 25 +++- .../ReactiveFindByIdOperationSupport.java | 32 +++-- .../ReactiveFindByQueryOperationSupport.java | 117 ++++++++-------- ...eFindFromReplicasByIdOperationSupport.java | 18 ++- .../ReactiveInsertByIdOperationSupport.java | 51 +++---- .../core/ReactiveRemoveByIdOperation.java | 5 +- .../ReactiveRemoveByIdOperationSupport.java | 49 ++++--- ...ReactiveRemoveByQueryOperationSupport.java | 57 ++++---- .../ReactiveReplaceByIdOperationSupport.java | 70 +++++----- .../core/ReactiveTemplateSupport.java | 8 +- .../ReactiveUpsertByIdOperationSupport.java | 28 ++-- .../data/couchbase/core/TemplateSupport.java | 14 +- .../couchbase/core/TransactionalSupport.java | 108 ++++++++------- .../BasicCouchbasePersistentEntity.java | 10 ++ .../mapping/CouchbasePersistentEntity.java | 4 + .../event/ReactiveAuditingEntityCallback.java | 2 +- .../couchbase/core/query/OptionsBuilder.java | 43 +++--- .../couchbase/core/support/PseudoArgs.java | 15 ++- .../data/couchbase/core/support/WithCas.java | 33 ----- .../core/support/WithTransaction.java | 30 ----- .../support/CouchbaseRepositoryBase.java | 10 +- .../support/DynamicInvocationHandler.java | 22 +-- .../support/SimpleCouchbaseRepository.java | 25 +--- .../SimpleReactiveCouchbaseRepository.java | 26 +--- .../support/TransactionResultHolder.java | 10 +- .../CouchbaseCallbackTransactionManager.java | 78 +++++------ .../transaction/CouchbaseResourceHolder.java | 18 ++- .../CouchbaseTransactionDefinition.java | 31 ++++- .../CouchbaseTransactionInterceptor.java | 5 +- .../CouchbaseTransactionStatus.java | 20 +++ .../CouchbaseTransactionalOperator.java | 12 +- ...TransactionRollbackRequestedException.java | 10 +- .../TransactionSystemAmbiguousException.java | 2 + .../TransactionSystemCouchbaseException.java | 32 ++--- ...TransactionSystemUnambiguousException.java | 11 +- ...gorizedTransactionDataAccessException.java | 29 ++-- ...chbaseCacheCollectionIntegrationTests.java | 15 +-- .../cache/CouchbaseCacheIntegrationTests.java | 5 - ...hbaseTemplateKeyValueIntegrationTests.java | 9 +- ...mplateQueryCollectionIntegrationTests.java | 100 +++++++------- ...ouchbaseTemplateQueryIntegrationTests.java | 15 +-- ...hbaseTemplateKeyValueIntegrationTests.java | 21 +-- ...mplateQueryCollectionIntegrationTests.java | 99 +++++++------- .../data/couchbase/domain/AbstractEntity.java | 5 +- .../data/couchbase/domain/Airport.java | 10 +- .../couchbase/domain/CollectionsConfig.java | 29 +++- .../data/couchbase/domain/Config.java | 2 +- .../domain/FluxIntegrationTests.java | 25 +--- .../data/couchbase/domain/Person.java | 19 +-- .../couchbase/domain/PersonRepository.java | 2 +- .../domain/PersonWithoutVersion.java | 11 +- .../domain/ReactiveAirportRepository.java | 5 +- .../domain/ReactivePersonRepository.java | 2 +- .../data/couchbase/domain/User.java | 13 -- ...aseRepositoryKeyValueIntegrationTests.java | 3 - ...aseRepositoryKeyValueIntegrationTests.java | 5 +- ...chbaseRepositoryQueryIntegrationTests.java | 2 +- ...sitoryQueryCollectionIntegrationTests.java | 7 +- ...sitoryQueryCollectionIntegrationTests.java | 16 +-- .../query/StringN1qlQueryCreatorTests.java | 3 - .../AfterTransactionAssertion.java | 50 ++++--- ...basePersonTransactionIntegrationTests.java | 44 +++--- ...onTransactionReactiveIntegrationTests.java | 28 +--- ...iveTransactionNativeIntegrationTests.java} | 25 ++-- ...aseTransactionNativeIntegrationTests.java} | 21 ++- ...onAllowableOperationsIntegrationTests.java | 36 +++-- ...ionalOperatorTemplateIntegrationTests.java | 29 ++-- ...eTransactionalOptionsIntegrationTests.java | 28 ++-- ...nsactionalPropagationIntegrationTests.java | 16 +-- ...ansactionalRepositoryIntegrationTests.java | 17 ++- ...TransactionalTemplateIntegrationTests.java | 66 ++++----- ...lUnsettableParametersIntegrationTests.java | 36 +++-- ...ormTransactionManagerIntegrationTests.java | 7 +- .../data/couchbase/transactions/ObjectId.java | 38 ++++-- .../couchbase/transactions/PersonService.java | 38 ++++-- .../transactions/PersonServiceReactive.java | 22 ++- ...TransactionalTemplateIntegrationTests.java | 12 +- .../transactions/ReplaceLoopThread.java | 125 ++++++++++-------- .../SimulateFailureException.java | 35 ++++- .../TransactionTemplateIntegrationTests.java | 4 +- .../transactions/TransactionsConfig.java | 30 ++++- ...onAllowableOperationsIntegrationTests.java | 17 ++- ...iveTransactionsPersonIntegrationTests.java | 51 ++++--- ...eTransactionsTemplateIntegrationTests.java | 49 +++---- ...onAllowableOperationsIntegrationTests.java | 10 +- ...KTransactionsTemplateIntegrationTests.java | 31 +++-- .../util/TransactionTestUtil.java | 24 ++-- .../util/ClusterAwareIntegrationTests.java | 11 +- .../couchbase/util/JavaIntegrationTests.java | 24 ++-- .../data/couchbase/util/TestCluster.java | 2 +- .../couchbase/util/TestClusterConfig.java | 2 + .../data/couchbase/util/Util.java | 42 +++--- src/test/resources/logback.xml | 2 +- 119 files changed, 1477 insertions(+), 1444 deletions(-) delete mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithCas.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java rename src/test/java/org/springframework/data/couchbase/transactions/{CouchbaseReactiveTransactionNativeTests.java => CouchbaseReactiveTransactionNativeIntegrationTests.java} (95%) rename src/test/java/org/springframework/data/couchbase/transactions/{CouchbaseTransactionNativeTests.java => CouchbaseTransactionNativeIntegrationTests.java} (97%) diff --git a/Jenkinsfile b/Jenkinsfile index 892e83be8..3b6378fca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -67,7 +67,7 @@ pipeline { steps { script { docker.withRegistry(p['docker.registry'], p['docker.credentials']) { - docker.image(p['docker.java.lts.image']).inside(p['docker.java.inside.basic']) { + docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml -Pci,artifactory -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-couchbase-non-root ' + '-Dartifactory.server=https://repo.spring.io ' + "-Dartifactory.username=${ARTIFACTORY_USR} " + diff --git a/pom.xml b/pom.xml index 5e574eca8..d91ffe679 100644 --- a/pom.xml +++ b/pom.xml @@ -233,17 +233,6 @@ 4.0.3 test - - org.testcontainers - testcontainers - - - - ch.qos.logback - logback-classic - 1.2.5 - compile - diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java index 9bbdc44ac..a200f963a 100644 --- a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -1,6 +1,6 @@ /* /* - * Copyright 2021 the original author or authors + * Copyright 2021-2022 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. @@ -16,8 +16,6 @@ */ package com.couchbase.client.java.transactions; -import java.lang.reflect.Field; - import com.couchbase.client.core.annotation.Stability; import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.codec.JsonSerializer; diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 875c48b92..c0299da65 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -18,9 +18,6 @@ import java.io.Closeable; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; @@ -31,8 +28,10 @@ /** * The {@link CouchbaseClientFactory} is the main way to get access to the managed SDK instance and resources. *

    - * Please note that a single factory is always bound to a {@link Bucket}, so if you need to access more than one - * you need to initialize one factory for each. + * Please note that a single factory is always bound to a {@link Bucket}, so if you need to access more than one you + * need to initialize one factory for each. + * + * @author Michael Reiche */ public interface CouchbaseClientFactory extends Closeable { diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index 06b16e017..d6ead366e 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -15,12 +15,8 @@ */ package org.springframework.data.couchbase; -import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.function.Supplier; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; @@ -32,11 +28,7 @@ import com.couchbase.client.java.ClusterOptions; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; -import com.couchbase.client.java.codec.JsonSerializer; import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; -import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; -import com.couchbase.client.java.transactions.config.TransactionsConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -52,18 +44,18 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { private final PersistenceExceptionTranslator exceptionTranslator; public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName) { + final String bucketName) { this(connectionString, authenticator, bucketName, null); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName) { + final String bucketName, final String scopeName) { this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator))), bucketName, scopeName); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName, final ClusterEnvironment environment) { + final String bucketName, final String scopeName, final ClusterEnvironment environment) { this( new OwnedSupplier<>( Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator).environment(environment))), @@ -75,7 +67,7 @@ public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketNa } private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, - final String scopeName) { + final String scopeName) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); @@ -131,8 +123,4 @@ public void close() { } } - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } - } 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 52e974201..7b0fbcd90 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -23,7 +23,6 @@ import java.util.HashSet; import java.util.Set; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -67,6 +66,7 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JacksonTransformers; import com.couchbase.client.java.json.JsonValueModule; +import com.couchbase.client.java.query.QueryScanConsistency; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -136,17 +136,11 @@ protected Authenticator authenticator() { public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) { return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); } -/* - @Bean - public ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory(final Cluster couchbaseCluster) { - return new SimpleReactiveCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); - } -*/ + @Bean(destroyMethod = "disconnect") public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment) { - Cluster c = Cluster.connect(getConnectionString(), + return Cluster.connect(getConnectionString(), clusterOptions(authenticator()).environment(couchbaseClusterEnvironment)); - return c; } @Bean(destroyMethod = "shutdown") @@ -171,29 +165,24 @@ 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, getDefaultConsistency()); + MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { + return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, + getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return couchbaseTemplate(couchbaseClientFactory, - mappingCouchbaseConverter, - new JacksonTranslationService()); + MappingCouchbaseConverter mappingCouchbaseConverter) { + return couchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); } @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( - CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, - couchbaseTranslationService, getDefaultConsistency()); + return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, + getDefaultConsistency()); } - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( - CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { return reactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); @@ -358,7 +347,8 @@ public CouchbaseTransactionalOperator transactionalOperator( @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionInterceptor transactionInterceptor(TransactionManager couchbaseTransactionManager) { TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); - TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(couchbaseTransactionManager, transactionAttributeSource); + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(couchbaseTransactionManager, + transactionAttributeSource); interceptor.setTransactionAttributeSource(transactionAttributeSource); if (couchbaseTransactionManager != null) { interceptor.setTransactionManager(couchbaseTransactionManager); diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index 014244fee..d5b68d01f 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -64,5 +64,5 @@ public class BeanNames { public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager"; - public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator" ; + public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 4fa6e1165..c33a53a7b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors + * Copyright 2022 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. @@ -41,6 +41,12 @@ import com.couchbase.client.core.error.CouchbaseException; + +/** + * Base shared by Reactive and non-Reactive TemplateSupport + * + * @author Michael Reiche + */ public abstract class AbstractTemplateSupport { final ReactiveCouchbaseTemplate template; diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java index b9d3a0564..5418182c8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseExceptionTranslator.java @@ -19,21 +19,39 @@ import java.util.ConcurrentModificationException; import java.util.concurrent.TimeoutException; -import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.dao.OptimisticLockingFailureException;; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.QueryTimeoutException; import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.dao.support.PersistenceExceptionTranslator; - -import com.couchbase.client.core.error.*; import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; +import com.couchbase.client.core.error.BucketNotFoundException; +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.CollectionNotFoundException; +import com.couchbase.client.core.error.ConfigException; +import com.couchbase.client.core.error.DecodingFailureException; +import com.couchbase.client.core.error.DesignDocumentNotFoundException; +import com.couchbase.client.core.error.DocumentExistsException; +import com.couchbase.client.core.error.DocumentLockedException; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.core.error.DurabilityAmbiguousException; +import com.couchbase.client.core.error.DurabilityImpossibleException; +import com.couchbase.client.core.error.DurabilityLevelNotAvailableException; +import com.couchbase.client.core.error.EncodingFailureException; +import com.couchbase.client.core.error.ReplicaNotConfiguredException; +import com.couchbase.client.core.error.RequestCanceledException; +import com.couchbase.client.core.error.ScopeNotFoundException; +import com.couchbase.client.core.error.ServiceNotAvailableException; +import com.couchbase.client.core.error.TemporaryFailureException; +import com.couchbase.client.core.error.ValueTooLargeException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; + /** * Simple {@link PersistenceExceptionTranslator} for Couchbase. *

    @@ -43,6 +61,8 @@ * * @author Michael Nitschinger * @author Simon Baslé + * @author Michael Reiche + * @author Graham Pople */ public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator { @@ -73,7 +93,7 @@ public final DataAccessException translateExceptionIfPossible(final RuntimeExcep return new OptimisticLockingFailureException(ex.getMessage(), ex); } - if ( ex instanceof ReplicaNotConfiguredException || ex instanceof DurabilityLevelNotAvailableException + if (ex instanceof ReplicaNotConfiguredException || ex instanceof DurabilityLevelNotAvailableException || ex instanceof DurabilityImpossibleException || ex instanceof DurabilityAmbiguousException) { return new DataIntegrityViolationException(ex.getMessage(), ex); } @@ -102,7 +122,7 @@ public final DataAccessException translateExceptionIfPossible(final RuntimeExcep if (ex instanceof TransactionOperationFailedException) { // Replace the TransactionOperationFailedException, since we want the Spring operation to fail with a - // Spring error. Internal state has already been set in the AttemptContext so the retry, rollback etc. + // Spring error. Internal state has already been set in the AttemptContext so the retry, rollback etc. // will get respected regardless of what gets propagated (or not) from the lambda. return new UncategorizedTransactionDataAccessException((TransactionOperationFailedException) ex); } 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 8cec31313..8e35fc4c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -20,12 +20,12 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.query.Query; -import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; - import com.couchbase.client.java.query.QueryScanConsistency; /** * Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}. + * + * @author Michael Reiche */ public interface CouchbaseOperations extends FluentCouchbaseOperations { @@ -41,6 +41,7 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { /** * The name of the scope used, null if the default scope is used. + * todo mr - not used? */ String getScopeName(); @@ -53,7 +54,8 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { * Returns the default consistency to use for queries */ QueryScanConsistency getConsistency(); - T save(T entity); + + T save(T entity, String... scopeAndCollection); Long count(Query query, Class domainType); 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 ce7f3b69e..e7a388110 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.core; -import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -55,27 +53,22 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final QueryScanConsistency scanConsistency; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final CouchbaseConverter converter) { - this(clientFactory, - converter, new JacksonTranslationService()); + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { + this(clientFactory, converter, new JacksonTranslationService()); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, - CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, - converter, translationService, null); + 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) { + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); - this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, - translationService, scanConsistency); + this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService, + scanConsistency); this.scanConsistency = scanConsistency; this.mappingContext = this.converter.getMappingContext(); @@ -87,16 +80,12 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, } } - public T save(T entity) { - if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { - return replaceById((Class) entity.getClass()).one(entity); - //} else if (getTransactionalOperator() != null) { - // return insertById((Class) entity.getClass()).one(entity); - } else { - return upsertById((Class) entity.getClass()).one(entity); - } + @Override + public T save(T entity, String... scopeAndCollection) { + return reactive().save(entity, scopeAndCollection).block(); } + @Override public Long count(Query query, Class domainType) { return findByQuery(domainType).matching(query).count(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 1169c98c7..10b47742a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -16,12 +16,6 @@ package org.springframework.data.couchbase.core; -import java.lang.reflect.InaccessibleObjectException; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -32,17 +26,11 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import org.springframework.data.couchbase.core.support.TemplateUtils; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.util.Assert; -import com.couchbase.client.core.error.CouchbaseException; - /** * Internal encode/decode support for CouchbaseTemplate. * @@ -58,7 +46,7 @@ class CouchbaseTemplateSupport extends AbstractTemplateSupport implements Applic private EntityCallbacks entityCallbacks; public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { super(template.reactive(), converter, translationService); this.template = template; } @@ -76,25 +64,13 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { @Override public T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txHolder) { - return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); - } - - @Override - public T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txHolder, CouchbaseResourceHolder holder) { + TransactionResultHolder txHolder, CouchbaseResourceHolder holder) { return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { - return applyResult(entity, converted, id, cas,txResultHolder, null); - } - - @Override - public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { return applyResultBase(entity, converted, id, cas, txResultHolder, holder); } @@ -103,7 +79,6 @@ public Integer getTxResultHolder(T source) { return null; } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index 1dcdf2dca..6c99bbc53 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -26,6 +26,11 @@ import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.analytics.AnalyticsScanConsistency; +/** + * {@link ExecutableFindByAnalyticsOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ExecutableFindByAnalyticsOperationSupport implements ExecutableFindByAnalyticsOperation { private static final AnalyticsQuery ALL_QUERY = new AnalyticsQuery(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 8d94a3a18..f7ad53140 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -26,6 +26,11 @@ import com.couchbase.client.java.kv.GetOptions; +/** + * ExecutableFindById Support + * + * @author Michael Reiche + */ public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOperation { private final CouchbaseTemplate template; @@ -37,7 +42,7 @@ public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOpe @Override public ExecutableFindById findById(Class domainType) { return new ExecutableFindByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType),null, null, null); + OptionsBuilder.getCollectionFrom(domainType), null, null, null); } static class ExecutableFindByIdSupport implements ExecutableFindById { @@ -52,7 +57,7 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry) { + GetOptions options, List fields, Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -61,8 +66,7 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { this.fields = fields; this.expiry = expiry; this.reactiveSupport = new ReactiveFindByIdSupport<>(template.reactive(), domainType, scope, collection, options, - fields, expiry, - new NonReactiveSupportWrapper(template.support())); + fields, expiry, new NonReactiveSupportWrapper(template.support())); } @Override @@ -83,12 +87,14 @@ public TerminatingFindById withOptions(final GetOptions options) { @Override public FindByIdWithOptions inCollection(final String collection) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection != null ? collection : this.collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, + collection != null ? collection : this.collection, options, fields, expiry); } @Override public FindByIdInCollection inScope(final String scope) { - return new ExecutableFindByIdSupport<>(template, domainType, scope != null ? scope : this.scope, collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope != null ? scope : this.scope, collection, + options, fields, expiry); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index f147dda67..ea6cb4819 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -287,7 +287,7 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 487caf476..01a0a7b8f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -63,8 +63,8 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final String[] fields; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, - final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, - final QueryOptions options, final String[] distinctFields, final String[] fields) { + final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, + final QueryOptions options, final String[] distinctFields, final String[] fields) { this.template = template; this.domainType = domainType; this.returnType = returnType; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 36193ae36..bd52efc02 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -27,6 +27,11 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +/** + * ExecutableInsertById Support + * + * @author Michael Reiche + */ public class ExecutableInsertByIdOperationSupport implements ExecutableInsertByIdOperation { private final CouchbaseTemplate template; @@ -57,8 +62,8 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -69,8 +74,7 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { this.durabilityLevel = durabilityLevel; this.expiry = expiry; this.reactiveSupport = new ReactiveInsertByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, - new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 47b5a8682..b52b4e2f6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -33,6 +33,7 @@ * Remove Operations on KV service. * * @author Christoph Strobl + * @author Michael Reiche * @since 2.0 */ public interface ExecutableRemoveByIdOperation { @@ -62,7 +63,7 @@ interface TerminatingRemoveById extends OneAndAllId { RemoveResult one(String id); /** - * Remove one document based on the entity. Transactions need the entity for the cas. + * Remove one document based on the entity. Transactions need the entity for the cas. * * @param entity the document ID. * @return result of the remove @@ -87,6 +88,7 @@ interface TerminatingRemoveById extends OneAndAllId { List allEntities(Collection entities); } + /** * Fluent method to specify options. */ diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 71ac65965..eb77a5458 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -27,6 +27,11 @@ import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +/** + * {@link ExecutableRemoveByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByIdOperation { private final CouchbaseTemplate template; @@ -45,8 +50,8 @@ public ExecutableRemoveById removeById() { public ExecutableRemoveById removeById(Class domainType) { return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + null); } static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { @@ -63,8 +68,8 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -76,7 +81,7 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); this.cas = cas; - } + } @Override public RemoveResult one(final String id) { @@ -98,11 +103,10 @@ public List allEntities(final Collection entities) { return reactiveRemoveByIdSupport.allEntities(entities).collectList().block(); } - @Override public RemoveByIdWithOptions inCollection(final String collection) { - return new ExecutableRemoveByIdSupport(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo, - durabilityLevel, cas); + return new ExecutableRemoveByIdSupport(template, domainType, scope, + collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override @@ -129,8 +133,8 @@ public TerminatingRemoveById withOptions(final RemoveOptions options) { @Override public RemoveByIdInCollection inScope(final String scope) { - return new ExecutableRemoveByIdSupport(template, domainType, scope != null ? scope : this.scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + return new ExecutableRemoveByIdSupport(template, domainType, scope != null ? scope : this.scope, collection, + options, persistTo, replicateTo, durabilityLevel, cas); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 7cab01cbd..77dbcdb90 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -25,6 +25,11 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +/** + * {@link ExecutableRemoveByQueryOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemoveByQueryOperation { private static final Query ALL_QUERY = new Query(); @@ -53,7 +58,7 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final QueryOptions options; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { this.template = template; this.domainType = domainType; this.query = query; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index c68493d21..122e268f3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -27,6 +27,11 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +/** + * {@link ExecutableReplaceByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ExecutableReplaceByIdOperationSupport implements ExecutableReplaceByIdOperation { private final CouchbaseTemplate template; @@ -57,8 +62,8 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -69,8 +74,7 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById this.durabilityLevel = durabilityLevel; this.expiry = expiry; this.reactiveSupport = new ReactiveReplaceByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, - new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 2fe9ef891..47fb7807f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,17 +15,18 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.convert.translation.TranslationService; -import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. * * @author Carlos Espinaco + * @author Michael Reiche * @since 4.2 */ public class NonReactiveSupportWrapper implements ReactiveTemplateSupport { @@ -41,24 +42,12 @@ public Mono encodeEntity(Object entityToEncode) { return Mono.fromSupplier(() -> support.encodeEntity(entityToEncode)); } - @Override - public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder) { - return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); - } - @Override public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } - @Override - public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder) { - return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); - } - @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { 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 f5b0478e7..2b2973a08 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -15,10 +15,11 @@ */ package org.springframework.data.couchbase.core; +import reactor.core.publisher.Mono; + import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.query.Query; -import reactor.core.publisher.Mono; import com.couchbase.client.java.query.QueryScanConsistency; @@ -51,12 +52,12 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper */ CouchbaseClientFactory getCouchbaseClientFactory(); - Mono save(T entity); + Mono save(T entity, String... scopeAndCollection); Mono count(Query query, Class personClass); - - /** - * @return the default consistency to use for queries - */ - QueryScanConsistency getConsistency(); + + /** + * @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 ba833ca4d..5e958b18d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -54,14 +54,12 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); private final QueryScanConsistency scanConsistency; - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final CouchbaseConverter converter) { + public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { this(clientFactory, converter, new JacksonTranslationService(), null); } - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final CouchbaseConverter converter, final TranslationService translationService, - final QueryScanConsistency scanConsistency) { + public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + final TranslationService translationService, final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -69,8 +67,10 @@ public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, this.scanConsistency = scanConsistency; } - public Mono save(T entity) { + public Mono save(T entity, String... scopeAndCollection) { Assert.notNull(entity, "Entity must not be null!"); + String scope = scopeAndCollection.length > 0 ? scopeAndCollection[0] : null; + String collection = scopeAndCollection.length > 1 ? scopeAndCollection[1] : null; Mono result; final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() .getPersistentEntity(entity.getClass()); @@ -84,13 +84,18 @@ public Mono save(T entity) { if (!versionPresent) { // the entity doesn't have a version property // No version field - no cas - result = (Mono) upsertById(clazz).one(entity); + // If in a transaction, insert is the only thing that will work + if (TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()) { + result = (Mono) insertById(clazz).inScope(scope).inCollection(collection).one(entity); + } else { // if not in a tx, then upsert will work + result = (Mono) upsertById(clazz).inScope(scope).inCollection(collection).one(entity); + } } else if (existingDocument) { // there is a version property, and it is non-zero // Updating existing document with cas - result = (Mono) replaceById(clazz).one(entity); + result = (Mono) replaceById(clazz).inScope(scope).inCollection(collection).one(entity); } else { // there is a version property, but it's zero or not set. // Creating new document - result = (Mono) insertById(clazz).one(entity); + result = (Mono) insertById(clazz).inScope(scope).inCollection(collection).one(entity); } return result; } @@ -233,57 +238,4 @@ public QueryScanConsistency getConsistency() { return scanConsistency; } - /** - * Value object chaining together a given source document with its mapped representation and the collection to persist - * it to. - * - * @param - * @author Christoph Strobl - * @since 2.2 - */ - /* - private static class PersistableEntityModel { - - private final T source; - private final @Nullable - Document target; - private final String collection; - - private PersistableEntityModel(T source, @Nullable Document target, String collection) { - - this.source = source; - this.target = target; - this.collection = collection; - } - - static PersistableEntityModel of(T source, String collection) { - return new PersistableEntityModel<>(source, null, collection); - } - - static PersistableEntityModel of(T source, Document target, String collection) { - return new PersistableEntityModel<>(source, target, collection); - } - - PersistableEntityModel mutate(T source) { - return new PersistableEntityModel(source, target, collection); - } - - PersistableEntityModel addTargetDocument(Document target) { - return new PersistableEntityModel(source, target, collection); - } - - T getSource() { - return source; - } - - @Nullable - Document getTarget() { - return target; - } - - String getCollection() { - return collection; - } - - */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index c2cd6d775..50d9b5c51 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,18 +16,6 @@ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.error.CouchbaseException; -import org.springframework.data.couchbase.core.support.TemplateUtils; -import reactor.core.publisher.Mono; - -import java.lang.reflect.InaccessibleObjectException; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; @@ -40,6 +28,8 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.util.Assert; @@ -48,6 +38,7 @@ * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. * * @author Carlos Espinaco + * @author Michael Reiche * @since 4.2 */ class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport @@ -57,7 +48,7 @@ class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport private ReactiveEntityCallbacks reactiveEntityCallbacks; public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { super(template, converter, translationService); this.template = template; } @@ -79,31 +70,18 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { } @Override - public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder) { - return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); - } - - @Override - public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { - return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); - } - - - @Override - public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder) { - return applyResult(entity, converted, id, cas, txResultHolder, null); + public Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, + String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + return Mono + .fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index b318fbf41..a166c4b1f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -32,6 +32,11 @@ import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.ExistsResult; +/** + * ReactiveExistsById Support + * + * @author Michael Reiche + */ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -72,12 +77,11 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, - domainType); - LOG.trace("existsById key={} {}", id, pArgs); - - return TransactionalSupport.verifyNotInTransaction("existsById") - .then(Mono.just(id)) + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("existsById key={} {}", id, pArgs); + } + return TransactionalSupport.verifyNotInTransaction("existsById").then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index 05ba33b22..21a074ddf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -18,6 +18,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.query.AnalyticsQuery; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.TemplateUtils; @@ -27,12 +29,19 @@ import com.couchbase.client.java.analytics.AnalyticsScanConsistency; import com.couchbase.client.java.analytics.ReactiveAnalyticsResult; +/** + * {@link ReactiveFindByAnalyticsOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAnalyticsOperation { private static final AnalyticsQuery ALL_QUERY = new AnalyticsQuery(); private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveFindByAnalyticsOperationSupport.class); + public ReactiveFindByAnalyticsOperationSupport(final ReactiveCouchbaseTemplate template) { this.template = template; } @@ -110,9 +119,11 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return TransactionalSupport.verifyNotInTransaction("findByAnalytics") - .then(template.getCouchbaseClientFactory().getCluster().reactive() - .analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> { + if (LOG.isDebugEnabled()) { + LOG.debug("findByAnalytics statement: {}", statement); + } + return TransactionalSupport.verifyNotInTransaction("findByAnalytics").then(template.getCouchbaseClientFactory() + .getCluster().reactive().analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { @@ -133,7 +144,7 @@ public Flux all() { } row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); - return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null); + return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null, null); }); }); } @@ -183,9 +194,11 @@ private String assembleEntityQuery(final boolean count) { final StringBuilder statement = new StringBuilder("SELECT "); if (count) { - statement.append("count(*) as __count"); + statement.append("count(*) as " + TemplateUtils.SELECT_COUNT); } else { - statement.append("meta().id as __id, meta().cas as __cas, ").append(bucket).append(".*"); + statement + .append("meta().id as " + TemplateUtils.SELECT_ID + ", meta().cas as " + TemplateUtils.SELECT_CAS + ", ") + .append(bucket).append(".*"); } final String dataset = support.getJavaNameForEntity(domainType); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 1151a9a4e..34a13226e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -18,7 +18,6 @@ import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,6 +32,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.util.Assert; import com.couchbase.client.core.error.DocumentNotFoundException; @@ -42,6 +42,11 @@ import com.couchbase.client.java.kv.GetAndTouchOptions; import com.couchbase.client.java.kv.GetOptions; +/** + * {@link ReactiveFindByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -69,8 +74,7 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, - ReactiveTemplateSupport support) { + CommonOptions options, List fields, Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -81,32 +85,29 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { this.support = support; } - - @Override public Mono one(final String id) { CommonOptions gOptions = initGetOptions(); PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType); - LOG.trace("findById key={} {}", id, pArgs); - + if (LOG.isDebugEnabled()) { + LOG.debug("findById key={} {}", id, pArgs); + } ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive(); Mono reactiveEntity = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { if (!ctxOpt.isPresent()) { - System.err.println("find no-tx"); if (pArgs.getOptions() instanceof GetAndTouchOptions) { return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection(), null)); + pArgs.getScope(), pArgs.getCollection(), null, null)); } else { return rc.get(id, (GetOptions) pArgs.getOptions()) .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection(), null)); + pArgs.getScope(), pArgs.getCollection(), null, null)); } } else { - System.err.println("find tx"); return ctxOpt.get().getCore().get(makeCollectionIdentifier(rc.async()), id) .flatMap(result -> support.decodeEntity(id, new String(result.contentAsBytes(), StandardCharsets.UTF_8), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), @@ -137,8 +138,7 @@ public Flux all(final Collection ids) { @Override public FindByIdInScope withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, - support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); } @Override @@ -157,14 +157,12 @@ public FindByIdInCollection inScope(final String scope) { public FindByIdInCollection project(String... fields) { Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), - expiry, - support); + expiry, support); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, - support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); } private CommonOptions initGetOptions() { 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 481802454..6ea6d595d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,12 +15,12 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.CouchbaseClientFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; @@ -55,8 +55,8 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, - OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, null, - null, template.support()); + OptionsBuilder.getScopeFrom(domainType), OptionsBuilder.getCollectionFrom(domainType), null, null, null, + template.support()); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -74,9 +74,9 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, - final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, + final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, + final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -102,16 +102,14 @@ public FindByQueryWithQuery matching(Query query) { scanCons = scanConsistency; } return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields, - support); + options, distinctFields, fields, support); } @Override public FindByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); + collection, options, distinctFields, fields, support); } @Override @@ -130,22 +128,19 @@ public FindByQueryWithDistinct inCollection(final String collection) { @Deprecated public FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); + collection, options, distinctFields, fields, support); } @Override public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); + collection, options, distinctFields, fields, support); } public FindByQueryWithProjecting as(Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); + collection, options, distinctFields, fields, support); } @Override @@ -153,8 +148,7 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, - support); + collection, options, distinctFields, fields, support); } @Override @@ -166,8 +160,7 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields, - support); + collection, options, dFields, fields, support); } @Override @@ -182,25 +175,25 @@ public Mono first() { @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, - domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); String statement = assembleEntityQuery(false, distinctFields, pArgs.getScope(), pArgs.getCollection()); - LOG.trace("findByQuery {} statement: {}", pArgs, statement); - + if (LOG.isDebugEnabled()) { + LOG.debug("findByQuery {} statement: {}", pArgs, statement); + } CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); Mono allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { - if (!s.isPresent()) { - QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), - clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); - } - }); + if (!s.isPresent()) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), + clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); + } + }); return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -213,22 +206,21 @@ public Flux all() { String id = ""; Long cas = Long.valueOf(0); if (!query.isDistinct() && distinctFields == null) { - id = row.getString(TemplateUtils.SELECT_ID); - if (id == null) { - id = row.getString(TemplateUtils.SELECT_ID_3x); - row.removeKey(TemplateUtils.SELECT_ID_3x); - } - cas = row.getLong(TemplateUtils.SELECT_CAS); - if (cas == null) { - cas = row.getLong(TemplateUtils.SELECT_CAS_3x); - row.removeKey(TemplateUtils.SELECT_CAS_3x); - } - row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); + id = row.getString(TemplateUtils.SELECT_ID); + if (id == null) { + id = row.getString(TemplateUtils.SELECT_ID_3x); + row.removeKey(TemplateUtils.SELECT_ID_3x); + } + cas = row.getLong(TemplateUtils.SELECT_CAS); + if (cas == null) { + cas = row.getLong(TemplateUtils.SELECT_CAS_3x); + row.removeKey(TemplateUtils.SELECT_CAS_3x); + } + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); } - System.err.println("row: "+row); return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), - null); + null, null); }); } @@ -244,27 +236,26 @@ private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { @Override public Mono count() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, - domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getScope(), pArgs.getCollection()); - LOG.trace("findByQuery {} statement: {}", pArgs, statement); + if (LOG.isDebugEnabled()) { + LOG.debug("findByQuery {} statement: {}", pArgs, statement); + } CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); Mono allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { - if (!s.isPresent()) { - System.err.println("count: no-tx"); - QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - System.err.println("count: tx"); - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), - clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); - } - }); + if (!s.isPresent()) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(), + clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts); + } + }); return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 28f833f98..f2f0880cd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -31,6 +31,11 @@ import com.couchbase.client.java.codec.RawJsonTranscoder; import com.couchbase.client.java.kv.GetAnyReplicaOptions; +/** + * {@link ReactiveFindFromReplicasByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFindFromReplicasByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -74,14 +79,15 @@ public Mono any(final String id) { if (garOptions.build().transcoder() == null) { garOptions.transcoder(RawJsonTranscoder.INSTANCE); } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, - domainType); - LOG.trace("getAnyReplica key={} {}", id, pArgs); - return TransactionalSupport.verifyNotInTransaction("findFromReplicasById") - .then(Mono.just(id)) + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("getAnyReplica key={} {}", id, pArgs); + } + return TransactionalSupport.verifyNotInTransaction("findFromReplicasById").then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, + pArgs.getScope(), pArgs.getCollection(), null, null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index b6d549b60..04285e4a3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -34,8 +36,11 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; - +/** + * {@link ReactiveInsertByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -67,9 +72,8 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, - ReactiveTemplateSupport support) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -84,22 +88,21 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, - domainType); - LOG.trace("insertById object={} {}", object, pArgs); - - return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection())).flatMap(collection -> support.encodeEntity(object) + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("insertById object={} {}", object, pArgs); + } + return Mono + .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection())) + .flatMap(collection -> support.encodeEntity(object) .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { if (!ctxOpt.isPresent()) { - System.err.println("insert non-tx"); return collection.reactive() .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap( - result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), null)); + .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), + null, null)); } else { rejectInvalidTransactionalOptions(); - System.err.println("insert tx"); return ctxOpt.get().getCore() .insert(makeCollectionIdentifier(collection.async()), converted.getId(), template.getCouchbaseClientFactory().getCluster().environment().transcoder() @@ -117,8 +120,10 @@ public Mono one(T object) { } private void rejectInvalidTransactionalOptions() { - if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { - throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) + || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException( + "withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); } if (this.expiry != null) { throw new IllegalArgumentException("withExpiry is not supported in a transaction"); @@ -141,8 +146,7 @@ public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override @@ -162,8 +166,7 @@ public InsertByIdWithOptions inCollection(final String collection) { public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override @@ -171,16 +174,14 @@ public InsertByIdInScope withDurability(final PersistTo persistTo, final Repl Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index a8d5cc8cc..ecdf6a061 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -35,6 +35,7 @@ * Remove Operations on KV service. * * @author Christoph Strobl + * @author Michael Reiche * @since 2.0 */ public interface ReactiveRemoveByIdOperation { @@ -70,6 +71,7 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { * @return result of the remove */ Mono oneEntity(Object entity); + /** * Remove the documents in the collection. * @@ -78,8 +80,9 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { */ @Override Flux all(Collection ids); + /** - * Remove the documents in the collection. Requires whole entity for transaction to have the cas. + * Remove the documents in the collection. Requires whole entity for transaction to have the cas. * * @param ids the document IDs. * @return result of the removes. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 252881612..9149f3616 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,9 +15,8 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.core.transaction.CoreTransactionGetResult; -import org.springframework.data.couchbase.CouchbaseClientFactory; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,18 +24,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; - +/** + * {@link ReactiveRemoveByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -72,8 +77,8 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final Long cas; ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -87,22 +92,20 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, - domainType); - LOG.trace("removeById key={} {}", id, pArgs); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("removeById key={} {}", id, pArgs); + } CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) - .reactive(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive(); return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> { if (!s.isPresent()) { - System.err.println("non-tx remove"); return rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); } else { rejectInvalidTransactionalOptions(); - System.err.println("tx remove"); - if ( cas == null || cas == 0 ){ + if (cas == null || cas == 0) { throw new IllegalArgumentException("cas must be supplied for tx remove"); } CoreTransactionAttemptContext ctx = s.get().getCore(); @@ -112,11 +115,11 @@ public Mono one(final String id) { if (getResult.cas() != cas) { return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas)); } - return ctx.remove(getResult) - .map(r -> new RemoveResult(id, 0, null)); + return ctx.remove(getResult).map(r -> new RemoveResult(id, 0, null)); }); - }}).onErrorMap(throwable -> { + } + }).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { @@ -126,8 +129,10 @@ public Mono one(final String id) { } private void rejectInvalidTransactionalOptions() { - if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { - throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) + || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException( + "withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); } if (this.options != null) { throw new IllegalArgumentException("withOptions is not supported in a transaction"); @@ -136,8 +141,8 @@ private void rejectInvalidTransactionalOptions() { @Override public Mono oneEntity(Object entity) { - ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, template.support().getCas(entity)); + ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, + persistTo, replicateTo, durabilityLevel, template.support().getCas(entity)); return op.one(template.support().getId(entity).toString()); } 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 2b4b63a7b..166afd810 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,27 +15,32 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode; -import com.couchbase.client.java.json.JsonObject; -import com.couchbase.client.java.transactions.TransactionQueryOptions; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.core.query.OptionsBuilder; import reactor.core.publisher.Flux; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; 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.core.deps.com.fasterxml.jackson.databind.node.ObjectNode; import com.couchbase.client.java.ReactiveScope; +import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +/** + * {@link ReactiveRemoveByQueryOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQueryOperation { private static final Query ALL_QUERY = new Query(); @@ -64,7 +69,7 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final QueryOptions options; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { this.template = template; this.domainType = domainType; this.query = query; @@ -78,30 +83,34 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery public Flux all() { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); String statement = assembleDeleteQuery(pArgs.getScope(), pArgs.getCollection()); - LOG.trace("removeByQuery {} statement: {}", pArgs, statement); + if (LOG.isDebugEnabled()) { + LOG.debug("removeByQuery {} statement: {}", pArgs, statement); + } CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); - return TransactionalSupport.checkForTransactionInThreadLocalStorage() - .flatMapMany(transactionContext -> { + return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMapMany(transactionContext -> { - if (!transactionContext.isPresent()) { - QueryOptions opts = buildQueryOptions(pArgs.getOptions()); - return (pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) - : rs.query(statement, opts)).flatMapMany(ReactiveQueryResult::rowsAsObject) + if (!transactionContext.isPresent()) { + QueryOptions opts = buildQueryOptions(pArgs.getOptions()); + return (pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts)).flatMapMany(ReactiveQueryResult::rowsAsObject) .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), Optional.empty())); - } else { - TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildQueryOptions(pArgs.getOptions())); - ObjectNode convertedOptions = com.couchbase.client.java.transactions.internal.OptionsUtil.createTransactionOptions(pArgs.getScope() == null ? null : rs, statement, opts); - return transactionContext.get().getCore().queryBlocking(statement, template.getBucketName(), pArgs.getScope(), convertedOptions, false) - .flatMapIterable(result -> result.rows).map(row -> { - JsonObject json = JsonObject.fromJson(row.data()); - return new RemoveResult(json.getString(TemplateUtils.SELECT_ID), - json.getLong(TemplateUtils.SELECT_CAS), Optional.empty()); - }); - } - }); + } else { + TransactionQueryOptions opts = OptionsBuilder + .buildTransactionQueryOptions(buildQueryOptions(pArgs.getOptions())); + ObjectNode convertedOptions = com.couchbase.client.java.transactions.internal.OptionsUtil + .createTransactionOptions(pArgs.getScope() == null ? null : rs, statement, opts); + return transactionContext.get().getCore() + .queryBlocking(statement, template.getBucketName(), pArgs.getScope(), convertedOptions, false) + .flatMapIterable(result -> result.rows).map(row -> { + JsonObject json = JsonObject.fromJson(row.data()); + return new RemoveResult(json.getString(TemplateUtils.SELECT_ID), json.getLong(TemplateUtils.SELECT_CAS), + Optional.empty()); + }); + } + }); } private QueryOptions buildQueryOptions(QueryOptions options) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 17c38d454..63507eb60 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -15,10 +15,8 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.core.transaction.CoreTransactionGetResult; -import com.couchbase.client.core.io.CollectionIdentifier; -import com.couchbase.client.core.transaction.util.DebugUtil; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,13 +30,20 @@ import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; +import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.core.transaction.util.DebugUtil; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; - +/** + * {@link ReactiveReplaceByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -70,9 +75,8 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReactiveTemplateSupport support; ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, - ReactiveTemplateSupport support) { + final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -87,39 +91,41 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, - domainType); - LOG.trace("replaceById object={} {}", object, pArgs); - - return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection())).flatMap(collection -> support.encodeEntity(object) + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("replaceById object={} {}", object, pArgs); + } + return Mono + .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection())) + .flatMap(collection -> support.encodeEntity(object) .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { if (!ctxOpt.isPresent()) { - System.err.println("replace: non-tx"); return collection.reactive() .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null)); + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, + null)); } else { - System.err.println("replace: tx"); rejectInvalidTransactionalOptions(); Long cas = support.getCas(object); - if ( cas == null || cas == 0 ){ - throw new IllegalArgumentException("cas must be supplied in object for tx replace. object="+object); + if (cas == null || cas == 0) { + throw new IllegalArgumentException( + "cas must be supplied in object for tx replace. object=" + object); } CollectionIdentifier collId = makeCollectionIdentifier(collection.async()); CoreTransactionAttemptContext ctx = ctxOpt.get().getCore(); - ctx.logger().info(ctx.attemptId(), "refetching %s for Spring replace", DebugUtil.docId(collId, converted.getId())); + ctx.logger().info(ctx.attemptId(), "refetching %s for Spring replace", + DebugUtil.docId(collId, converted.getId())); Mono gr = ctx.get(collId, converted.getId()); return gr.flatMap(getResult -> { if (getResult.cas() != cas) { return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas)); } - return ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().environment().transcoder() - .encode(converted.export()).encoded()); + return ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().environment() + .transcoder().encode(converted.export()).encoded()); }).flatMap(result -> this.support.applyResult(object, converted, converted.getId(), 0L, null, null)); } })).onErrorMap(throwable -> { @@ -132,8 +138,10 @@ public Mono one(T object) { } private void rejectInvalidTransactionalOptions() { - if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { - throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) + || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException( + "withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); } if (this.expiry != null) { throw new IllegalArgumentException("withExpiry is not supported in a transaction"); @@ -157,8 +165,7 @@ private ReplaceOptions buildReplaceOptions(ReplaceOptions options, T object, Cou public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override @@ -178,8 +185,7 @@ public ReplaceByIdInCollection inScope(final String scope) { public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override @@ -187,16 +193,14 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, - support); + durabilityLevel, expiry, support); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index cd5fd8c23..1339ea62f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -23,21 +23,17 @@ import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** + * ReactiveTemplateSupport + * * @author Michael Reiche */ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder); - Mono decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); - Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder); - Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index aaae1a370..dbf27bb3c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -33,6 +33,11 @@ import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.kv.UpsertOptions; +/** + * {@link ReactiveUpsertByIdOperation} implementations for Couchbase. + * + * @author Michael Reiche + */ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -64,8 +69,8 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { private final ReactiveTemplateSupport support; ReactiveUpsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { + final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -80,16 +85,19 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, - domainType); - LOG.trace("upsertById object={} {}", object, pArgs); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("upsertById object={} {}", object, pArgs); + } Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction("upsertById") - .then(support.encodeEntity(object)) - .flatMap(converted -> { - return Mono.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection())).flatMap(collection -> collection.reactive() + .then(support.encodeEntity(object)).flatMap(converted -> { + return Mono + .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection())) + .flatMap(collection -> collection.reactive() .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + .flatMap( + result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null))); }); return reactiveEntity.onErrorMap(throwable -> { diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 0192ea7fb..973a10930 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -17,25 +17,21 @@ import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** - * * @author Michael Reiche */ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - - T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); + T decodeEntity(String id, String source, Long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); - T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); - - T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, CouchbaseResourceHolder holder); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, + CouchbaseResourceHolder holder); Long getCas(Object entity); @@ -43,8 +39,6 @@ public interface TemplateSupport { String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); - Integer getTxResultHolder(T source); TranslationService getTranslationService(); diff --git a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java index 631249838..6b56e8647 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java @@ -1,57 +1,75 @@ +/* + * Copyright 2022 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; -import com.couchbase.client.core.error.CasMismatchException; -import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.core.transaction.threadlocal.TransactionMarkerOwner; -import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import reactor.core.publisher.Mono; import java.util.Optional; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; + import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.threadlocal.TransactionMarkerOwner; +/** + * Utility methods to support transactions. + * + * @author Graham Pople + */ @Stability.Internal public class TransactionalSupport { - /** - * Returns non-empty iff in a transaction. It determines this from thread-local storage and/or reactive context. - *

    - * The user could be doing a reactive operation (with .block()) inside a blocking transaction (like @Transactional). - * Or a blocking operation inside a ReactiveTransactionsWrapper transaction (which would be a bad idea). - * So, need to check both thread-local storage and reactive context. - */ - public static Mono> checkForTransactionInThreadLocalStorage() { - return TransactionMarkerOwner.get().flatMap(markerOpt -> { - Optional out = markerOpt - .flatMap(marker -> Optional.of(new CouchbaseResourceHolder(marker.context()))); - return Mono.just(out); - }); - } - - public static Mono verifyNotInTransaction(String methodName) { - return checkForTransactionInThreadLocalStorage() - .flatMap(s -> { - if (s.isPresent()) { - return Mono.error(new IllegalArgumentException(methodName + "can not be used inside a transaction")); - } - else { - return Mono.empty(); - } - }); - } - - public static RuntimeException retryTransactionOnCasMismatch(CoreTransactionAttemptContext ctx, long cas1, long cas2) { - try { - ctx.logger().info(ctx.attemptId(), "Spring CAS mismatch %s != %s, retrying transaction", cas1, cas2); - TransactionOperationFailedException err = TransactionOperationFailedException.Builder.createError() - .retryTransaction() - .cause(new CasMismatchException(null)) - .build(); - return ctx.operationFailed(err); - } catch (Throwable err) { - return new RuntimeException(err); - } - - } + /** + * Returns non-empty iff in a transaction. It determines this from thread-local storage and/or reactive context. + *

    + * The user could be doing a reactive operation (with .block()) inside a blocking transaction (like @Transactional). + * Or a blocking operation inside a ReactiveTransactionsWrapper transaction (which would be a bad idea). So, need to + * check both thread-local storage and reactive context. + */ + public static Mono> checkForTransactionInThreadLocalStorage() { + return TransactionMarkerOwner.get().flatMap(markerOpt -> { + Optional out = markerOpt + .flatMap(marker -> Optional.of(new CouchbaseResourceHolder(marker.context()))); + return Mono.just(out); + }); + } + + public static Mono verifyNotInTransaction(String methodName) { + return checkForTransactionInThreadLocalStorage().flatMap(s -> { + if (s.isPresent()) { + return Mono.error(new IllegalArgumentException(methodName + "can not be used inside a transaction")); + } else { + return Mono.empty(); + } + }); + } + + public static RuntimeException retryTransactionOnCasMismatch(CoreTransactionAttemptContext ctx, long cas1, + long cas2) { + try { + ctx.logger().info(ctx.attemptId(), "Spring CAS mismatch %s != %s, retrying transaction", cas1, cas2); + TransactionOperationFailedException err = TransactionOperationFailedException.Builder.createError() + .retryTransaction().cause(new CasMismatchException(null)).build(); + return ctx.operationFailed(err); + } catch (Throwable err) { + return new RuntimeException(err); + } + + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index 0a4efc58a..b25e7d5ec 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -165,4 +165,14 @@ public boolean isTouchOnRead() { .getAnnotation(org.springframework.data.couchbase.core.mapping.Document.class); return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } + + @Override + public boolean hasTextScoreProperty() { + return false; + } + + @Override + public CouchbasePersistentProperty getTextScoreProperty() { + return null; + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index 3ae142faa..17a53cd3e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -61,4 +61,8 @@ public interface CouchbasePersistentEntity extends PersistentEntity domainType) { return null; } + /** + * collection annotation + * + * @param domainType + * @return + */ public static String getCollectionFrom(Class domainType) { if (domainType == null) { return null; diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index 7edad7d5e..d70f6390d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -17,16 +17,15 @@ import static org.springframework.data.couchbase.core.query.OptionsBuilder.fromFirst; -import com.couchbase.client.core.error.CouchbaseException; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.io.CollectionIdentifier; /** - * determine the arguments to be used in the operation from various sources + * Determine the arguments to be used in the operation from various sources * * @author Michael Reiche - * * @param */ public class PseudoArgs { @@ -45,14 +44,15 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * 1) values from fluent api
    * 2) values from dynamic proxy (via template threadLocal)
    * 3) the values from the couchbaseClientFactory
    - * @param template which holds ThreadLocal pseudo args + * + * @param template which holds ThreadLocal pseudo args * @param scope - from calling operation * @param collection - from calling operation * @param options - from calling operation * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - Class domainType) { + Class domainType) { String scopeForQuery = null; String collectionForQuery = null; @@ -110,8 +110,9 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; - if( scopeForQuery != null && collectionForQuery == null){ - throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); + if (scopeForQuery != null && collectionForQuery == null) { + throw new CouchbaseException( + new IllegalArgumentException("if scope is not default or null, then collection must be specified")); } this.options = optionsForQuery; diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java deleted file mode 100644 index 1399235a3..000000000 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 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.support; - -import com.couchbase.client.java.query.QueryScanConsistency; - -/** - * A common interface operations that take scan consistency - * - * @author Michael Reiche - * @param - the entity class - */ -public interface WithCas { - /** - * Specify scan consistency - * - * @param cas - */ - Object withCas(Long cas); -} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java deleted file mode 100644 index d86691ac9..000000000 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 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.support; - -/** - * Interface for operations that take distinct fields - * - * @author Michael Reiche - * @param - the entity class - */ -public interface WithTransaction { - /** - * Specify transactions - * - */ - Object transaction(); -} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index 40ee839b5..0fd7f5cbc 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -27,6 +27,14 @@ import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.java.query.QueryScanConsistency; +/** + * Common base for SimpleCouchbaseRepository and SimpleReactiveCouchbaseRepository + * + * @param + * @param + * + * @author Michael Reiche + */ public class CouchbaseRepositoryBase { /** @@ -37,7 +45,7 @@ public class CouchbaseRepositoryBase { private CrudMethodMetadata crudMethodMetadata; public CouchbaseRepositoryBase(CouchbaseEntityInformation entityInformation, - Class repositoryInterface) { + Class repositoryInterface) { this.entityInformation = entityInformation; this.repositoryInterface = repositoryInterface; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index cde109dcf..d2a547607 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2022 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. @@ -55,10 +55,9 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) this.target).getOperations(); this.entityInformation = ((ReactiveCouchbaseRepository) this.target).getEntityInformation(); } else { - if( CouchbaseRepository.class.isAssignableFrom(target.getClass())) - System.err.println("isAssignable"); printInterfaces(target.getClass(), " "); - throw new RuntimeException("Unknown target type: " + target.getClass()); + throw new RuntimeException("Unknown target type: " + target.getClass() + + " CouchbaseRepository.class.isAssignable:" + CouchbaseRepository.class.isAssignableFrom(target.getClass())); } this.options = options; this.collection = collection; @@ -66,15 +65,16 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle this.repositoryClass = target.getClass(); } - void printInterfaces(Class clazz, String tab){ - System.out.println(tab+"{"); - for(Class c:clazz.getInterfaces()){ - System.out.println(tab+" " +c.getSimpleName()); - if(c.getInterfaces().length > 0) - printInterfaces(c, tab+" "); + void printInterfaces(Class clazz, String tab) { + System.err.println(tab + "{"); + for (Class c : clazz.getInterfaces()) { + System.err.println(tab + " " + c.getSimpleName()); + if (c.getInterfaces().length > 0) + printInterfaces(c, tab + " "); } - System.out.println(tab+"}"); + System.err.println(tab + "}"); } + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index 4632e33e3..4ce408c8d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -35,9 +35,9 @@ import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.util.ReflectionUtils; /** * Repository base implementation for Couchbase. @@ -71,28 +71,7 @@ public SimpleCouchbaseRepository(CouchbaseEntityInformation entityInf @Override @SuppressWarnings("unchecked") public S save(S entity) { - Assert.notNull(entity, "Entity must not be null!"); - S result; - - final CouchbasePersistentEntity mapperEntity = operations.getConverter().getMappingContext() - .getPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); - final boolean versionPresent = versionProperty != null; - final Long version = versionProperty == null || versionProperty.getField() == null ? null - : (Long) ReflectionUtils.getField(versionProperty.getField(), entity); - final boolean existingDocument = version != null && version > 0; - - if (!versionPresent) { // the entity doesn't have a version property - // No version field - no cas - result = (S) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); - } else if (existingDocument) { // there is a version property, and it is non-zero - // Updating existing document with cas - result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); - } else { // there is a version property, but it's zero or not set. - // Creating new document - result = (S) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); - } - return result; + return operations.save(entity, getScope(), getCollection()); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 778924fe2..b23f9d3b4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -63,7 +63,7 @@ public class SimpleReactiveCouchbaseRepository extends CouchbaseRepositor * @param operations the reference to the reactive template used. */ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation entityInformation, - ReactiveCouchbaseOperations operations, Class repositoryInterface) { + ReactiveCouchbaseOperations operations, Class repositoryInterface) { super(entityInformation, repositoryInterface); this.operations = operations; } @@ -97,27 +97,7 @@ public Flux saveAll(Publisher entityStream) { @SuppressWarnings("unchecked") private Mono save(S entity, String scope, String collection) { - Assert.notNull(entity, "Entity must not be null!"); - Mono result; - final CouchbasePersistentEntity mapperEntity = operations.getConverter().getMappingContext() - .getPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); - final boolean versionPresent = versionProperty != null; - final Long version = versionProperty == null || versionProperty.getField() == null ? null - : (Long) ReflectionUtils.getField(versionProperty.getField(), entity); - final boolean existingDocument = version != null && version > 0; - - if (!versionPresent) { // the entity doesn't have a version property - // No version field - no cas - result = (Mono) operations.upsertById(getJavaType()).inScope(scope).inCollection(collection).one(entity); - } else if (existingDocument) { // there is a version property, and it is non-zero - // Updating existing document with cas - result = (Mono) operations.replaceById(getJavaType()).inScope(scope).inCollection(collection).one(entity); - } else { // there is a version property, but it's zero or not set. - // Creating new document - result = (Mono) operations.insertById(getJavaType()).inScope(scope).inCollection(collection).one(entity); - } - return result; + return operations.save(entity, scope, collection); } @Override @@ -214,7 +194,7 @@ public Mono deleteAllById(Iterable ids) { @Override public Mono deleteAll(Iterable entities) { return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) - .allEntities((java.util.Collection)(Streamable.of(entities).toList())).then(); + .allEntities((java.util.Collection) (Streamable.of(entities).toList())).then(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java index 653bba57f..e429ff89a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -16,10 +16,10 @@ package org.springframework.data.couchbase.repository.support; +import reactor.util.annotation.Nullable; + import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.query.QueryResult; -import com.couchbase.client.java.transactions.TransactionGetResult; -import reactor.util.annotation.Nullable; /** * Holds previously obtained Transaction*Result @@ -36,8 +36,8 @@ public TransactionResultHolder(CoreTransactionGetResult getResult) { // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). // todo gp will need to expose a copy ctor if a copy is needed this.getResult = getResult; -// this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), -// getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); + // this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), + // getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); this.singleQueryResult = null; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java index 427a3d3e4..e6befd9d9 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors + * Copyright 2022 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. @@ -15,11 +15,14 @@ */ package org.springframework.data.couchbase.transaction; -import com.couchbase.client.core.annotation.Stability; -import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.client.java.transactions.config.TransactionOptions; -import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -35,16 +38,17 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; import org.springframework.transaction.support.TransactionCallback; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException; +import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * The Couchbase transaction manager, providing support for @Transactional methods. + * + * @author Graham Pople */ public class CouchbaseCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { @@ -61,7 +65,8 @@ public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClien * This override is for users manually creating a CouchbaseCallbackTransactionManager, and allows the * TransactionOptions to be overridden. */ - public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory, @Nullable TransactionOptions options) { + public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory, + @Nullable TransactionOptions options) { this.couchbaseClientFactory = couchbaseClientFactory; this.options = options != null ? options : TransactionOptions.transactionOptions(); } @@ -107,7 +112,8 @@ private T executeNewTransaction(TransactionCallback callback) { T res = callback.doInTransaction(status); if (res instanceof Mono || res instanceof Flux) { - throw new UnsupportedOperationException("Return type is Mono or Flux, indicating a reactive transaction is being performed in a blocking way. A potential cause is the CouchbaseTransactionInterceptor is not in use."); + throw new UnsupportedOperationException( + "Return type is Mono or Flux, indicating a reactive transaction is being performed in a blocking way. A potential cause is the CouchbaseTransactionInterceptor is not in use."); } execResult.set(res); @@ -117,8 +123,7 @@ private T executeNewTransaction(TransactionCallback callback) { }, this.options); return execResult.get(); - } - catch (RuntimeException ex) { + } catch (RuntimeException ex) { throw convert(ex); } } @@ -134,7 +139,8 @@ private static RuntimeException convert(RuntimeException ex) { return ex; } - private Flux executeNewReactiveTransaction(org.springframework.transaction.reactive.TransactionCallback callback) { + private Flux executeNewReactiveTransaction( + org.springframework.transaction.reactive.TransactionCallback callback) { // Buffer the output rather than attempting to stream results back from a now-defunct lambda. final List out = new ArrayList<>(); @@ -164,25 +170,20 @@ public boolean isCompleted() { } }; - return Flux.from(callback.doInTransaction(status)) - .doOnNext(v -> out.add(v)) - .then(Mono.defer(() -> { - if (status.isRollbackOnly()) { - return Mono - .error(new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set")); - } - return Mono.empty(); - })); + return Flux.from(callback.doInTransaction(status)).doOnNext(v -> out.add(v)).then(Mono.defer(() -> { + if (status.isRollbackOnly()) { + return Mono.error(new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set")); + } + return Mono.empty(); + })); }); - }, this.options) - .thenMany(Flux.defer(() -> Flux.fromIterable(out))) - .onErrorMap(ex -> { - if (ex instanceof RuntimeException) { - return convert((RuntimeException) ex); - } - return ex; - }); + }, this.options).thenMany(Flux.defer(() -> Flux.fromIterable(out))).onErrorMap(ex -> { + if (ex instanceof RuntimeException) { + return convert((RuntimeException) ex); + } + return ex; + }); } // Propagation defines what happens when a @Transactional method is called from another @Transactional method. @@ -270,16 +271,19 @@ public TransactionStatus getTransaction(@Nullable TransactionDefinition definiti // the transaction manager is a CallbackPreferringPlatformTransactionManager. // So these methods should only be hit if user is using PlatformTransactionManager directly. Spring supports this, // but due to the lambda-based nature of our transactions, we cannot. - throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + throw new UnsupportedOperationException( + "Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); } @Override public void commit(TransactionStatus ignored) throws TransactionException { - throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + throw new UnsupportedOperationException( + "Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); } @Override public void rollback(TransactionStatus ignored) throws TransactionException { - throw new UnsupportedOperationException("Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); + throw new UnsupportedOperationException( + "Direct programmatic use of the Couchbase PlatformTransactionManager is not supported"); } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java index 68f1030c6..2b3881885 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2022 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. @@ -15,16 +15,22 @@ */ package org.springframework.data.couchbase.transaction; -import com.couchbase.client.core.annotation.Stability; +import java.util.HashMap; +import java.util.Map; + import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; +import com.couchbase.client.core.annotation.Stability; import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import java.util.HashMap; -import java.util.Map; - +/** + * Container for couchbase transaction resources to hold in threadlocal or reactive context. + * + * @author Michael Reiche + * + */ @Stability.Internal public class CouchbaseResourceHolder extends ResourceHolderSupport { @@ -37,7 +43,6 @@ public class CouchbaseResourceHolder extends ResourceHolderSupport { * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. */ public CouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) { - this.core = core; } @@ -50,7 +55,6 @@ public CoreTransactionAttemptContext getCore() { } public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { - System.err.println("PUT: "+System.identityHashCode(o)+" "+o); getResultMap.put(System.identityHashCode(o), holder); return holder; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java index 6bb5936b8..58ab56f79 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -1,12 +1,33 @@ +/* + * Copyright 2022 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.transaction; -import com.couchbase.client.core.annotation.Stability; import org.springframework.transaction.support.DefaultTransactionDefinition; +import com.couchbase.client.core.annotation.Stability; + +/** + * Couchbase Transaction Definition for Spring Data transaction framework. + * + * @author Michael Reiche + */ @Stability.Internal public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition { - public CouchbaseTransactionDefinition(){ - super(); - setIsolationLevel(ISOLATION_READ_COMMITTED); - } + public CouchbaseTransactionDefinition() { + super(); + setIsolationLevel(ISOLATION_READ_COMMITTED); + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java index df613f3db..746973919 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2022 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. @@ -36,6 +36,9 @@ *

    * The solution: override the standard TransactionInterceptor and, if the * CouchbaseCallbackTransactionManager is the provided TransactionManager, defer to that. + * + * @author Graham Pople + * @author Michael Reiche */ @Stability.Internal public class CouchbaseTransactionInterceptor extends TransactionInterceptor diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java index 516c2fcec..af4a3eecf 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java @@ -1,7 +1,27 @@ +/* + * Copyright 2012-2022 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.transaction; import org.springframework.transaction.support.DefaultTransactionStatus; +/** + * Couchbase transaction status for Spring Data transaction framework. + * + * @author Graham Pople + */ public class CouchbaseTransactionStatus extends DefaultTransactionStatus { /** diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java index d1a06e479..c6bc295b7 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors + * Copyright 2022 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. @@ -15,17 +15,20 @@ */ package org.springframework.data.couchbase.transaction; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.reactive.TransactionCallback; import org.springframework.transaction.reactive.TransactionalOperator; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * The TransactionalOperator interface is another method to perform reactive transactions with Spring. *

    * We recommend instead using a regular reactive SDK transaction, and performing Spring operations inside it. + * + * @author Graham Pople */ public class CouchbaseTransactionalOperator implements TransactionalOperator { private final CouchbaseCallbackTransactionManager manager; @@ -40,8 +43,7 @@ public static CouchbaseTransactionalOperator create(CouchbaseCallbackTransaction @Override public Mono transactional(Mono mono) { - return transactional(Flux.from(mono)) - .singleOrEmpty(); + return transactional(Flux.from(mono)).singleOrEmpty(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java index 05841b319..ae7a0f9c9 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionRollbackRequestedException.java @@ -19,9 +19,11 @@ /** * A transaction rollback has been requested. + * + * @author Graham Pople */ -public class TransactionRollbackRequestedException extends CouchbaseException { - public TransactionRollbackRequestedException(String message) { - super(message); - } +public class TransactionRollbackRequestedException extends CouchbaseException { + public TransactionRollbackRequestedException(String message) { + super(message); + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java index 2053e8a55..eb424e977 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemAmbiguousException.java @@ -32,6 +32,8 @@ * * An asynchronous cleanup process will try to complete the transaction: roll it back if it didn't commit, roll it * forwards if it did. + * + * @author Graham Pople */ public class TransactionSystemAmbiguousException extends TransactionSystemCouchbaseException { public TransactionSystemAmbiguousException(TransactionCommitAmbiguousException ex) { diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java index 367cacac1..e66097c4d 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemCouchbaseException.java @@ -17,27 +17,29 @@ import java.util.List; +import org.springframework.transaction.TransactionSystemException; + import com.couchbase.client.core.cnc.events.transaction.TransactionLogEvent; import com.couchbase.client.java.transactions.error.TransactionFailedException; -import org.springframework.transaction.TransactionSystemException; /** - * A base class of transaction-level exceptions raised by Couchbase, allowing them to be handled in - * one place. + * A base class of transaction-level exceptions raised by Couchbase, allowing them to be handled in one place. + * + * @author Graham Pople */ abstract public class TransactionSystemCouchbaseException extends TransactionSystemException { - private final TransactionFailedException internal; + private final TransactionFailedException internal; - public TransactionSystemCouchbaseException(TransactionFailedException ex) { - super(ex.getMessage(), ex.getCause()); - this.internal = ex; - } + public TransactionSystemCouchbaseException(TransactionFailedException ex) { + super(ex.getMessage(), ex.getCause()); + this.internal = ex; + } - /** - * An in-memory log is built up during each transaction. The application may want to write this to their own logs, - * for example upon transaction failure. - */ - public List logs() { - return internal.logs(); - } + /** + * An in-memory log is built up during each transaction. The application may want to write this to their own logs, for + * example upon transaction failure. + */ + public List logs() { + return internal.logs(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java index 676da52eb..c21fcab4d 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/TransactionSystemUnambiguousException.java @@ -18,13 +18,14 @@ import com.couchbase.client.java.transactions.error.TransactionFailedException; /** - * The transaction failed and unambiguously did not commit. No actors can see any part of this failed - * transaction. + * The transaction failed and unambiguously did not commit. No actors can see any part of this failed transaction. *

    * The application does not need to do anything to rollback the transaction. + * + * @author Graham Pople */ public class TransactionSystemUnambiguousException extends TransactionSystemCouchbaseException { - public TransactionSystemUnambiguousException(TransactionFailedException ex) { - super(ex); - } + public TransactionSystemUnambiguousException(TransactionFailedException ex) { + super(ex); + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java index 9165d88ca..9d9159770 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/error/UncategorizedTransactionDataAccessException.java @@ -18,26 +18,29 @@ import org.springframework.dao.UncategorizedDataAccessException; import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; -//import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException; +import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException; /** * An opaque signal that something went wrong during the execution of an operation inside a transaction. *

    * The application is not expected to catch or inspect this exception, and should allow it to propagate. *

    - * Internal state has been set that ensures that the transaction will act appropriately (including rolling - * back and retrying if necessary) regardless of what the application does with this exception. + * Internal state has been set that ensures that the transaction will act appropriately (including rolling back and + * retrying if necessary) regardless of what the application does with this exception. + * + * @author Graham Pople */ -public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException implements WrappedTransactionOperationFailedException { - private final TransactionOperationFailedException internal; +public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException + implements WrappedTransactionOperationFailedException { + private final TransactionOperationFailedException internal; - public UncategorizedTransactionDataAccessException(TransactionOperationFailedException err) { - super(err.getMessage(), err.getCause()); - this.internal = err; - } + public UncategorizedTransactionDataAccessException(TransactionOperationFailedException err) { + super(err.getMessage(), err.getCause()); + this.internal = err; + } - @Override - public TransactionOperationFailedException wrapped() { - return internal; - } + @Override + public TransactionOperationFailedException wrapped() { + return internal; + } } diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index af25691ee..a12962e8c 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -16,21 +16,18 @@ package org.springframework.data.couchbase.cache; +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import java.util.UUID; - -import static org.junit.Assert.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; /** * CouchbaseCache tests Theses tests rely on a cb server running. diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index 3d814ac37..e9fa46ceb 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -36,8 +36,6 @@ import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.couchbase.client.java.query.QueryOptions; - /** * CouchbaseCache tests Theses tests rely on a cb server running. * @@ -70,8 +68,6 @@ public void afterEach() { super.afterEach(); } - - @Test void cachePutGet() { CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1"); @@ -127,7 +123,6 @@ void cachePutIfAbsent() { assertEquals(user1, cache.get(user1.getId()).get()); // user1.getId() is still user1 } - @Test // this WORKS public void clearWithDelayOk() throws InterruptedException { cache.put("KEY", "VALUE"); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 1adcd7ff9..38af1b2aa 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -35,11 +35,9 @@ import java.util.Set; import java.util.UUID; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById; @@ -59,19 +57,16 @@ import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; import org.springframework.data.couchbase.domain.UserSubmission; -import org.springframework.data.couchbase.transactions.CouchbaseReactiveTransactionNativeTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -; /** * KV tests Theses tests rely on a cb server running. @@ -108,7 +103,7 @@ void findByIdWithExpiry() { User foundUser = couchbaseTemplate.findById(User.class).withExpiry(Duration.ofSeconds(1)).one(user1.getId()); user1.setVersion(foundUser.getVersion());// version will have changed assertEquals(user1, foundUser); - sleepMs(2000); + sleepMs(3000); Collection foundUsers = (Collection) couchbaseTemplate.findById(User.class) .all(Arrays.asList(user1.getId(), user2.getId())); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 7be304ea4..131670a94 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -61,7 +61,6 @@ import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; -import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.kv.ExistsOptions; @@ -142,8 +141,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -185,8 +184,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } @@ -210,8 +209,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(daveUsers) - .all(); + .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(daveUsers).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -227,12 +226,12 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); - final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS) - .inCollection(collectionName).matching(specialUsers) + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) + .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers) .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); @@ -252,8 +251,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -277,8 +276,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(nonSpecialUsers) - .all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(nonSpecialUsers).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -301,18 +300,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -320,8 +319,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count2); } finally { @@ -345,20 +344,19 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all().collectList() - .block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all().collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class) .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .count().block(); + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block(); assertEquals(2, count1); // count( distinct (all fields in icaoClass) // which only has one field @@ -366,8 +364,8 @@ void distinctReactive() { Class icaoClass = (new Object() { String icao; }).getClass(); - long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .count().block(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block(); assertEquals(7, count2); } finally { @@ -440,8 +438,8 @@ public void findByQuery() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("441")); try { - List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()); @@ -492,9 +490,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("495")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -583,8 +581,8 @@ public void findByQueryOther() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("594")); try { - List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(otherScope).inCollection(otherCollection).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()); @@ -635,9 +633,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("648")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(otherScope).inCollection(otherCollection).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -700,8 +698,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all()); + assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class) + .withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection).withOptions(options).all()); } @Test @@ -739,9 +737,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all()); + () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all()); } @Test @@ -765,8 +763,8 @@ public void testScopeCollectionAnnotation() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).matching(query).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); @@ -785,8 +783,8 @@ public void testScopeCollectionRepoWith() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).matching(query).all(); assertEquals(saved, found.get(0), "should have found what was saved"); couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) .all(); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index b1f15371a..d91d0d35b 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -69,8 +69,7 @@ @SpringJUnitConfig(Config.class) class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { - @Autowired - public CouchbaseTemplate couchbaseTemplate; + @Autowired public CouchbaseTemplate couchbaseTemplate; @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeEach @@ -137,8 +136,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).matching(specialUsers) - .all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) + .matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } @@ -151,8 +150,8 @@ void findAssessmentDO() { ado = couchbaseTemplate.upsertById(AssessmentDO.class).one(ado); Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); - final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class).withConsistency(REQUEST_PLUS) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) + .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); assertEquals("123", foundUsers.get(0).getId(), "id"); assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); assertEquals(ado, foundUsers.get(0)); @@ -346,8 +345,8 @@ void sortedTemplate() { .query(QueryCriteria.where("iata").isNotNull()); Pageable pageableWithSort = PageRequest.of(0, 7, Sort.by("iata")); query.with(pageableWithSort); - List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).matching(query) - .all(); + List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .matching(query).all(); String[] sortedIatas = iatas.clone(); System.out.println("" + iatas.length + " " + sortedIatas.length); diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index 265ad44df..f57f4625a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -38,7 +39,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ReactiveFindByIdOperation.ReactiveFindById; @@ -58,11 +58,10 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * KV tests Theses tests rely on a cb server running. @@ -81,9 +80,14 @@ class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationT @Override public void beforeEach() { super.beforeEach(); - List r1 = reactiveCouchbaseTemplate.removeByQuery(User.class).all().collectList().block(); - List r2 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated.class).all().collectList().block(); - List r3 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated2.class).all().collectList().block(); + List r1 = reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all() + .collectList().block(); + List r2 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated.class).withConsistency(REQUEST_PLUS) + .all().collectList().block(); + List r3 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated2.class).withConsistency(REQUEST_PLUS) + .all().collectList().block(); + List f3 = reactiveCouchbaseTemplate.findByQuery(UserAnnotated2.class).withConsistency(REQUEST_PLUS) + .all().collectList().block(); } @Test @@ -99,15 +103,14 @@ void findByIdWithExpiry() { .one(user1.getId()).block(); user1.setVersion(foundUser.getVersion());// version will have changed assertEquals(user1, foundUser); - sleepMs(2000); + sleepMs(3000); Collection foundUsers = (Collection) reactiveCouchbaseTemplate.findById(User.class) .all(Arrays.asList(user1.getId(), user2.getId())).collectList().block(); assertEquals(1, foundUsers.size(), "should have found exactly 1 user"); assertEquals(user2, foundUsers.iterator().next()); } finally { - reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all() - .collectList().block(); + reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all().collectList().block(); } } diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index f4b7fe5ca..7a35aa70c 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -81,8 +81,7 @@ @SpringJUnitConfig(CollectionsConfig.class) class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { - @Autowired - public CouchbaseTemplate couchbaseTemplate; + @Autowired public CouchbaseTemplate couchbaseTemplate; @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; Airport vie = new Airport("airports::vie", "vie", "low80"); @@ -113,11 +112,11 @@ public void beforeEach() { couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).all(); template = reactiveCouchbaseTemplate; } @@ -141,8 +140,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -184,8 +183,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } @@ -209,8 +208,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(daveUsers) - .all(); + .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(daveUsers).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -226,12 +225,12 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); - final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class).withConsistency(REQUEST_PLUS) - .inCollection(collectionName).matching(specialUsers) + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) + .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers) .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); @@ -248,8 +247,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS) + .inCollection(collectionName).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -273,8 +272,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(nonSpecialUsers) - .all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) + .matching(nonSpecialUsers).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -297,18 +296,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -316,8 +315,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(icaoClass).withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).inCollection(collectionName).count(); assertEquals(7, count2); } finally { @@ -341,20 +340,19 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all().collectList() - .block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .all().collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName) - .count().block(); + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block(); assertEquals(2, count1); // count( distinct { iata, icao } ) @@ -432,8 +430,8 @@ public void findByQuery() { // 4 Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowa")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) + .inCollection(collectionName).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()).block(); @@ -484,9 +482,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowe")).block(); - List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all().collectList().block(); + List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(scopeName).inCollection(collectionName).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -574,8 +572,8 @@ public void findByQueryOther() { // 4 Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowj")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); @@ -626,9 +624,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lown")).block(); - List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all().collectList().block(); + List removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS) + .inScope(otherScope).inCollection(otherCollection).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -691,8 +689,9 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all().collectList().block()); + assertThrows(AmbiguousTimeoutException.class, + () -> template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options).all().collectList().block()); } @Test @@ -730,9 +729,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options) .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) - .all().collectList().block()); + () -> template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope) + .inCollection(otherCollection).withOptions(options) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java index 43ca51c36..e46dae102 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -24,6 +24,7 @@ /** * @author Oliver Gierke + * @author Michael Reiche */ @Document public class AbstractEntity { @@ -39,7 +40,7 @@ public UUID getId() { return id; } - public String id(){ + public String id() { return id.toString(); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index 74a88d09b..cda300bf6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -17,6 +17,7 @@ package org.springframework.data.couchbase.domain; import jakarta.validation.constraints.Max; + import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; @@ -44,8 +45,7 @@ public class Airport extends ComparableEntity { @CreatedBy private String createdBy; @Expiration private long expiration; - @Max(2) - long size; + @Max(2) long size; private long someNumber; @PersistenceConstructor @@ -92,11 +92,11 @@ public String getCreatedBy() { return createdBy; } - public long getSize(){ + public long getSize() { return size; } - public void setSize(long size){ + public void setSize(long size) { this.size = size; } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java index ae3588c9b..d9a926b20 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java +++ b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java @@ -1,8 +1,29 @@ +/* + * Copyright 2012-2022 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.domain; +/** + * Config to be used for testing scopes and collections. + * + * @author Michael Reiche + */ public class CollectionsConfig extends Config { - @Override - public String getScopeName(){ - return "my_scope"; - } + @Override + public String getScopeName() { + return "my_scope"; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index e41ddc264..825840738 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java index a1def5a59..d38ffca1b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/domain/FluxIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors + * Copyright 2021-2022 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. @@ -15,22 +15,8 @@ */ package org.springframework.data.couchbase.domain; -import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryProfile; -import com.couchbase.client.java.query.QueryResult; -import com.couchbase.client.java.query.QueryScanConsistency; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.core.RemoveResult; -import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterType; -import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.data.util.Pair; +import static org.junit.jupiter.api.Assertions.assertEquals; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ParallelFlux; @@ -43,8 +29,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -76,8 +61,6 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.query.QueryScanConsistency; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * @author Michael Reiche */ diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index 34c5a0184..639ae4b5e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -26,10 +26,14 @@ import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.domain.Persistable; import org.springframework.lang.Nullable; +/** + * Person entity for tests. + * + * @author Michael Reiche + */ @Document public class Person extends AbstractEntity implements Persistable { Optional firstname; @@ -52,9 +56,8 @@ public class Person extends AbstractEntity implements Persistable { @Transient private boolean isNew; - public Person() { - setId( UUID.randomUUID()); + setId(UUID.randomUUID()); } public Person(String firstname, String lastname) { @@ -174,7 +177,7 @@ public Person withFirstName(String firstName) { // A with-er that returns the same object ?? public Person withVersion(Long version) { - //Person p = new Person(this.getId(), this.getFirstname(), this.getLastname()); + // Person p = new Person(this.getId(), this.getFirstname(), this.getLastname()); this.version = version; return this; } @@ -198,12 +201,12 @@ public boolean isNew() { return isNew; } - public void isNew(boolean isNew){ + public void isNew(boolean isNew) { this.isNew = isNew; } - public Person withIdFirstname() { + public Person withIdFirstname() { return this.withFirstName(getId().toString()); - } + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index b6f23ebe2..baa832a8c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java index ff4c60971..bb4fdaf34 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -19,11 +19,14 @@ import java.util.UUID; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.lang.Nullable; +/** + * Person entity without a an @Version property + * + * @author Michael Reiche + */ @Document -public class PersonWithoutVersion extends AbstractEntity -{ +public class PersonWithoutVersion extends AbstractEntity { Optional firstname; Optional lastname; diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index a2aaaa7b4..786c4ea7c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.domain; -import lombok.val; -import org.springframework.data.couchbase.core.query.WithConsistency; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -48,9 +46,8 @@ public interface ReactiveAirportRepository extends ReactiveCouchbaseRepository, DynamicProxyable { - @Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}") - @ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS) + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Flux findIdByDynamicN1ql(String docType, String queryStatement); @Override diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java index e28666513..e87974e0f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index a3cc29760..5a841edb8 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -29,19 +29,6 @@ import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; -import java.io.Serializable; -import java.util.Objects; - -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.data.annotation.Transient; -import org.springframework.data.annotation.TypeAlias; -import org.springframework.data.annotation.Version; -import org.springframework.data.couchbase.core.mapping.Document; - /** * User entity for tests * diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 4d46078dd..87ec74ae5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -30,9 +30,6 @@ import java.util.Optional; import java.util.UUID; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.java.env.ClusterEnvironment; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index f46e9c01e..d15268e98 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.auditing.DateTimeProvider; @@ -82,11 +81,11 @@ void saveReplaceUpsertInsert() { Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null); // save the document - we don't care how on this call reactiveAirlineRepository.save(airline).block(); - reactiveAirlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. + reactiveAirlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or + // replace. reactiveAirlineRepository.delete(airline).block(); } - @Test void saveAndFindById() { User user = new User(UUID.randomUUID().toString(), "saveAndFindById_reactive", "l"); diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 4809e221c..815f9a4c1 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -106,7 +106,7 @@ void testQuery() { jfk = new Airport("airports::jfk", "JFK", "xxxx"); reactiveAirportRepository.save(jfk).block(); - List all = reactiveAirportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); + List all = reactiveAirportRepository.findIdByDynamicN1ql("", "").toStream().collect(Collectors.toList()); System.out.println(all); assertFalse(all.isEmpty()); assertTrue(all.stream().anyMatch(a -> a.equals("airports::vie"))); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 0b4cb0e19..027eb33c1 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -40,7 +40,6 @@ import org.springframework.data.couchbase.domain.AirportRepository; import org.springframework.data.couchbase.domain.AirportRepositoryAnnotated; import org.springframework.data.couchbase.domain.CollectionsConfig; -import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserCol; import org.springframework.data.couchbase.domain.UserColRepository; @@ -290,8 +289,7 @@ void findPlusN1qlJoinBothAnnotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName) - .all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName).all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionAnnotatedRepository.findByUsername(user.getUsername()); @@ -345,8 +343,7 @@ void findPlusN1qlJoinUnannotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName) - .all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(REQUEST_PLUS).inScope(scopeName).all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionUnannotatedRepository.findByUsername(user.getUsername()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 003f5b71c..3ae641c2f 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -31,7 +31,6 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.CollectionsConfig; -import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveAirportRepositoryAnnotated; import org.springframework.data.couchbase.domain.ReactiveUserColRepository; @@ -48,7 +47,6 @@ import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Reactive Repository Query Tests with Collections @@ -227,9 +225,10 @@ void stringDeleteCollectionTest() { Airport otherAirport = new Airport(loc(), "xxx", "xyz"); try { airport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(airport).block(); - otherAirport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(otherAirport).block(); - assertEquals(1, - reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).deleteByIata(airport.getIata()).collectList().block().size()); + otherAirport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(otherAirport) + .block(); + assertEquals(1, reactiveAirportRepository.withScope(scopeName).withCollection(collectionName) + .deleteByIata(airport.getIata()).collectList().block().size()); } catch (Exception e) { e.printStackTrace(); throw e; @@ -246,7 +245,8 @@ void stringDeleteWithRepositoryAnnotationTest() { airport = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(airport).block(); otherAirport = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(otherAirport).block(); // don't specify a collection - should get collection from AirportRepositoryAnnotated - assertEquals(1, reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIata(airport.getIata()).collectList().block().size()); + assertEquals(1, reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIata(airport.getIata()) + .collectList().block().size()); } catch (Exception e) { e.printStackTrace(); throw e; @@ -264,8 +264,8 @@ void stringDeleteWithMethodAnnotationTest() { Airport airportSaved = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(airport).block(); Airport otherAirportSaved = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(otherAirport).block(); // don't specify a collection - should get collection from deleteByIataAnnotated method - assertThrows(IndexFailureException.class, () -> assertEquals(1, - reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIataAnnotated(airport.getIata()).collectList().block().size())); + assertThrows(IndexFailureException.class, () -> assertEquals(1, reactiveAirportRepositoryAnnotated + .withScope(scopeName).deleteByIataAnnotated(airport.getIata()).collectList().block().size())); } catch (Exception e) { e.printStackTrace(); throw e; diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index a29d532c2..cebcdc1d4 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -29,7 +29,6 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.domain.Airline; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.mapping.context.MappingContext; @@ -44,8 +43,6 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; -import com.couchbase.client.java.query.QueryScanConsistency; - /** * @author Michael Nitschinger * @author Michael Reiche diff --git a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java index 1771e909e..54484ff92 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java @@ -1,3 +1,19 @@ +/* + * Copyright 2012-2022 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.transactions; import lombok.Data; @@ -5,29 +21,29 @@ import org.springframework.data.domain.Persistable; /** - * @author Christoph Strobl - * @currentRead Shadow's Edge - Brent Weeks + * For testing transactions. + * + * @author Michael Reiche */ @Data public class AfterTransactionAssertion { - private final T persistable; - private boolean expectToBePresent; + private final T persistable; + private boolean expectToBePresent; - public void isPresent() { - expectToBePresent = true; - } + public void isPresent() { + expectToBePresent = true; + } - public void isNotPresent() { - expectToBePresent = false; - } + public void isNotPresent() { + expectToBePresent = false; + } - public Object getId() { - return persistable.getId(); - } + public Object getId() { + return persistable.getId(); + } - public boolean shouldBePresent() { - return expectToBePresent; - } + public boolean shouldBePresent() { + return expectToBePresent; + } } - diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index d1127fc22..34fa22e9b 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -18,27 +18,21 @@ import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; 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 com.couchbase.client.core.error.DocumentExistsException; import lombok.Data; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Disabled; -import org.springframework.data.couchbase.core.TransactionalSupport; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; -import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import reactor.core.publisher.Mono; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; @@ -46,10 +40,13 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.TransactionalSupport; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -57,8 +54,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.reactive.TransactionalOperator; +import com.couchbase.client.core.error.DocumentExistsException; import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * Tests for com.couchbase.transactions using @@ -82,7 +79,7 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT String sName = "_default"; String cName = "_default"; - + Person WalterWhite; @BeforeAll @@ -105,25 +102,26 @@ public void beforeEachTest() { WalterWhite = new Person("Walter", "White"); TransactionTestUtil.assertNotInTransaction(); List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); List rp2 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); - List rp3 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List rp3 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); List p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) .all(); List e0 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); - List e1 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List e1 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); } @DisplayName("rollback after exception using transactionalOperator") @Test public void shouldRollbackAfterException() { - assertThrowsWithCause(() -> personService.savePersonErrors(WalterWhite), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(() -> personService.savePersonErrors(WalterWhite), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); assertEquals(0, count, "should have done roll back and left 0 entries"); } @@ -131,7 +129,8 @@ public void shouldRollbackAfterException() { @Test @DisplayName("rollback after exception using @Transactional") public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { - assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite), TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite), + TransactionSystemUnambiguousException.class, SimulateFailureException.class); Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); assertEquals(0, count, "should have done roll back and left 0 entries"); } @@ -239,7 +238,7 @@ public void insertPersonCBTransactionsRxTmplRollback() { .doOnNext(ppp -> TransactionalSupport.checkForTransactionInThreadLocalStorage() .doOnNext(v -> assertTrue(v.isPresent()))) .map(p -> throwSimulateFailureException(p)).as(transactionalOperator::transactional); // tx - assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class); Person pFound = cbTmpl.findById(Person.class).one(WalterWhite.id()); assertNull(pFound, "insert should have been rolled back"); } @@ -305,8 +304,7 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds"); AtomicInteger tryCount = new AtomicInteger(); - Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) - .block(); + Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount).block(); Person pFound = cbTmpl.findById(Person.class).one(person.id()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index dbed77627..088841fa5 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -17,35 +17,27 @@ package org.springframework.data.couchbase.transactions; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; import lombok.Data; -import org.springframework.data.couchbase.core.RemoveResult; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.util.LinkedList; import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -54,13 +46,12 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.client.java.transactions.error.TransactionFailedException; /** - * Tests for com.couchbase.transactions without using the spring data transactions framework - *

    - * todo gp: these tests are using the `.as(transactionalOperator::transactional)` method which is for the chopping block, so presumably these tests are too + * todo gp: these tests are using the `.as(transactionalOperator::transactional)` method which is for the chopping + * block, so presumably these tests are too + * todo mr: I'm not sure how as(transactionalOperator::transactional) is different than + * todo mr: transactionOperator.transaction(...)in CouchbaseTransactionalOperatorTemplateIntegrationTests ? * * @author Michael Reiche */ @@ -136,10 +127,6 @@ public void commitShouldPersistTxEntries() { } @Test - /* todo - does this need to be in - Caused by: java.lang.UnsupportedOperationException: Return type is Mono or Flux, indicating a reactive transaction - is being performed in a blocking way. A potential cause is the CouchbaseSimpleTransactionInterceptor is not in use. - */ public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { personService.declarativeSavePerson(WalterWhite).as(StepVerifier::create) // @@ -219,7 +206,6 @@ public void errorAfterTxShouldNotAffectPreviousStep() { .verifyComplete(); } - @Data // @AllArgsConstructor static class EventLog { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java similarity index 95% rename from src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java rename to src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java index 4f38849e3..cf20134f3 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -21,9 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.couchbase.client.java.transactions.error.TransactionFailedException; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -41,6 +38,8 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -56,7 +55,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) -public class CouchbaseReactiveTransactionNativeTests extends JavaIntegrationTests { +public class CouchbaseReactiveTransactionNativeIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired ReactivePersonRepository rxRepo; @@ -89,8 +88,8 @@ public void beforeEachTest() { TransactionTestUtil.assertNotInTransaction(); TransactionTestUtil.assertNotInTransaction(); List rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); List p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); List p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) .all(); @@ -112,7 +111,8 @@ public void replacePersonRbTemplate() { Flux result = txOperator.execute((ctx) -> rxCBTmpl.findById(Person.class).one(person.id()) .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) .map(it -> throwSimulateFailureException(it))); - assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, + SimulateFailureException.class); Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); assertEquals(person, pFound, "Should have found " + person); } @@ -133,7 +133,8 @@ public void insertPersonRbTemplate() { Flux result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).one(person) .flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))) .map(it -> throwSimulateFailureException(it))); - assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, + SimulateFailureException.class); Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block(); assertNull(pFound, "Should NOT have found " + pFound); } @@ -144,7 +145,8 @@ public void replacePersonRbRepo() { Flux result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).findById(person.id()) .flatMap(p -> rxRepo.withCollection(cName).save(p.withFirstName("Walt"))) .flatMap(it -> Mono.error(new SimulateFailureException()))); - assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, + SimulateFailureException.class); Person pFound = rxRepo.withCollection(cName).findById(person.id()).block(); assertEquals(person, pFound, "Should have found " + person); } @@ -154,7 +156,8 @@ public void insertPersonRbRepo() { Person person = WalterWhite; Flux result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).save(person) // insert .map(it -> throwSimulateFailureException(it))); - assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, SimulateFailureException.class); + assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class, + SimulateFailureException.class); Person pFound = rxRepo.withCollection(cName).findById(person.id()).block(); assertNull(pFound, "Should NOT have found " + pFound); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java similarity index 97% rename from src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java rename to src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java index c06014dc1..b37fd692d 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -20,18 +20,12 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; -import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; -import org.springframework.transaction.TransactionManager; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; -import org.springframework.transaction.reactive.TransactionalOperator; - import java.util.Optional; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -40,13 +34,16 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; +import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.reactive.TransactionalOperator; /** * Tests for com.couchbase.transactions without using the spring data transactions framework @@ -61,7 +58,7 @@ // form of TransactionalOperator. Also there does not seem to be a need for a CouchbaseTransactionalOperator as // TransactionalOperator.create(reactiveCouchbaseTransactionManager) seems to work just fine. (I don't recall what // merits the "Native" in the name). -public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { +public class CouchbaseTransactionNativeIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired TransactionManager couchbaseTransactionManager; @Autowired PersonRepository repo; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java index 49345ee81..6ff59cc81 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -16,18 +16,21 @@ package org.springframework.data.couchbase.transactions; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -36,24 +39,18 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - /** - * Tests for @Transactional methods, where operations that aren't supported in a transaction are being used. - * They should be prevented at runtime. + * Tests for @Transactional methods, where operations that aren't supported in a transaction are being used. They should + * be prevented at runtime. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = {TransactionsConfig.class, CouchbaseTransactionalNonAllowableOperationsIntegrationTests.PersonService.class}) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + CouchbaseTransactionalNonAllowableOperationsIntegrationTests.PersonService.class }) public class CouchbaseTransactionalNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -75,7 +72,7 @@ public void beforeEachTest() { void test(Consumer r) { AtomicInteger tryCount = new AtomicInteger(0); -assertThrowsWithCause( () -> { + assertThrowsWithCause(() -> { personService.doInTransaction(tryCount, (ops) -> { r.accept(ops); return null; @@ -120,8 +117,7 @@ public void upsertById() { @Service @Component @EnableTransactionManagement - static - class PersonService { + static class PersonService { final CouchbaseOperations personOperations; public PersonService(CouchbaseOperations ops) { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java index 2a9b01737..55bdbbc14 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOperatorTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,6 +42,7 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -50,11 +50,10 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.reactive.TransactionalOperator; -import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.client.java.transactions.error.TransactionFailedException; - /** * Tests for CouchbaseTransactionalOperator, using template methods (findById etc.) + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) @@ -95,8 +94,7 @@ public RunResult(int attempts) { } private RunResult doMonoInTransaction(Supplier> lambda) { - CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( - couchbaseClientFactory); + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory); TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); AtomicInteger attempts = new AtomicInteger(); @@ -110,8 +108,7 @@ private RunResult doMonoInTransaction(Supplier> lambda) { @DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.execute should succeed") @Test public void committedInsertWithExecute() { - CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( - couchbaseClientFactory); + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory); TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); operator.execute(v -> { @@ -127,8 +124,7 @@ public void committedInsertWithExecute() { @DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.transactional(Flux) should succeed") @Test public void committedInsertWithFlux() { - CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager( - couchbaseClientFactory); + CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory); TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager); Flux flux = Flux.defer(() -> { @@ -195,8 +191,8 @@ public void committedRemoveByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doMonoInTransaction(() -> { - return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(person.id())) - .all().next(); + return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.id())).all().next(); }); Person fetched = blocking.findById(Person.class).one(person.id()); @@ -210,8 +206,8 @@ public void committedFindByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doMonoInTransaction(() -> { - return ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(person.id())) - .all().next(); + return ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(person.id())).all().next(); }); assertEquals(1, rr.attempts); @@ -258,8 +254,7 @@ public void rollbackRemove() { assertThrowsWithCause(() -> doMonoInTransaction(() -> { attempts.incrementAndGet(); - return ops.findById(Person.class).one(person.id()) - .flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // + return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // .doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result }), TransactionSystemUnambiguousException.class, SimulateFailureException.class); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java index 453d187c9..90ecde159 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -16,18 +16,18 @@ package org.springframework.data.couchbase.transactions; -import com.couchbase.client.core.error.transaction.AttemptExpiredException; -import com.couchbase.client.java.transactions.error.TransactionExpiredException; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Person; @@ -39,24 +39,20 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.TransactionTimedOutException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import com.couchbase.client.core.error.transaction.AttemptExpiredException; /** * Tests for @Transactional methods, setting all the various options allowed by @Transactional. + * + * @author Graham Pople */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalOptionsIntegrationTests.PersonService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalOptionsIntegrationTests.PersonService.class }) public class CouchbaseTransactionalOptionsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java index 96da1b42f..dcb00c5e8 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalPropagationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -17,11 +17,8 @@ package org.springframework.data.couchbase.transactions; 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.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; @@ -41,7 +38,7 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -50,21 +47,20 @@ import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.transaction.NoTransactionException; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.couchbase.client.core.error.transaction.RetryTransactionException; -import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * Tests for the various propagation values allowed on @Transactional methods. + * + * @author Graham Pople */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalPropagationIntegrationTests.PersonService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalPropagationIntegrationTests.PersonService.class }) public class CouchbaseTransactionalPropagationIntegrationTests extends JavaIntegrationTests { private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionalPropagationIntegrationTests.class); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java index 15aad9714..a899d65ce 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.fail; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; @@ -34,28 +33,27 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import com.couchbase.client.java.transactions.error.TransactionFailedException; - /** * Tests @Transactional with repository methods. + * + * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalRepositoryIntegrationTests.UserService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalRepositoryIntegrationTests.UserService.class }) public class CouchbaseTransactionalRepositoryIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired UserRepository userRepo; @@ -115,7 +113,8 @@ public void save() { public void saveRolledBack() { String id = UUID.randomUUID().toString(); - assertThrowsWithCause( () -> {; + assertThrowsWithCause(() -> { + ; userService.run(repo -> { User user = repo.save(new User(id, "Ada", "Lovelace")); SimulateFailureException.throwEx("fail"); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java index 06a15a9d3..12d324515 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -16,8 +16,21 @@ package org.springframework.data.couchbase.transactions; -import com.couchbase.client.core.error.transaction.AttemptExpiredException; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -34,6 +47,7 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonWithoutVersion; import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -41,33 +55,19 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; +import com.couchbase.client.core.error.transaction.AttemptExpiredException; /** * Tests for @Transactional, using template methods (findById etc.) + * + * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - CouchbaseTransactionalTemplateIntegrationTests.PersonService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateIntegrationTests.PersonService.class }) public class CouchbaseTransactionalTemplateIntegrationTests extends JavaIntegrationTests { // intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated. @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -94,7 +94,8 @@ public void beforeEachTest() { List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); List pwovr = operations.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all(); - List pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all(); + List pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS) + .all(); } @AfterEach @@ -234,7 +235,7 @@ public void rollbackRemove() { @Test public void rollbackRemoveByQuery() { AtomicInteger tryCount = new AtomicInteger(); - Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); + Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname()); assertThrowsWithCause(() -> { personService.doInTransaction(tryCount, ops -> { @@ -315,8 +316,8 @@ public void replacePerson() { operations.replaceById(Person.class).one(refetched); assertNotEquals(person.getVersion(), refetched.getVersion()); AtomicInteger tryCount = new AtomicInteger(0); - assertThrowsWithCause(() -> personService.replace(person, tryCount), - TransactionSystemUnambiguousException.class, AttemptExpiredException.class); + assertThrowsWithCause(() -> personService.replace(person, tryCount), TransactionSystemUnambiguousException.class, + AttemptExpiredException.class); } @DisplayName("Entity must have CAS field during replace") @@ -334,11 +335,11 @@ public void replaceEntityWithoutCas() { public void replaceEntityWithCasZero() { Person person = operations.insertById(Person.class).one(WalterWhite); // switchedPerson here will have CAS=0, which will fail - Person switchedPerson = new Person( "Dave", "Reynolds"); + Person switchedPerson = new Person("Dave", "Reynolds"); AtomicInteger tryCount = new AtomicInteger(0); - assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount), TransactionSystemUnambiguousException.class, - IllegalArgumentException.class); + assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount), + TransactionSystemUnambiguousException.class, IllegalArgumentException.class); } @DisplayName("Entity must have CAS field during remove") @@ -355,7 +356,7 @@ public void removeEntityWithoutCas() { @Test public void removeEntityById() { AtomicInteger tryCount = new AtomicInteger(); - Person person = operations.insertById(Person.class).one(WalterWhite); + Person person = operations.insertById(Person.class).one(WalterWhite); assertThrowsWithCause(() -> { personService.doInTransaction(tryCount, (ops) -> { Person p = ops.findById(Person.class).one(person.id()); @@ -410,12 +411,11 @@ public Person declarativeSavePersonWithThread(Person person, Thread thread) { @Transactional public void insertThenThrow() { assertInAnnotationTransaction(true); - Person person = personOperations.insertById(Person.class).one(new Person( "Walter", "White")); + Person person = personOperations.insertById(Person.class).one(new Person("Walter", "White")); SimulateFailureException.throwEx(); } - @Autowired - CouchbaseCallbackTransactionManager callbackTm; + @Autowired CouchbaseCallbackTransactionManager callbackTm; /** * to execute while ThreadReplaceloop() is running should force a retry diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java index 1099f79e8..2a1c18c36 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -16,23 +16,22 @@ package org.springframework.data.couchbase.transactions; -import com.couchbase.client.java.kv.InsertOptions; -import com.couchbase.client.java.kv.PersistTo; -import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.client.java.kv.ReplaceOptions; -import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -40,23 +39,20 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import com.couchbase.client.java.kv.InsertOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.kv.ReplaceOptions; +import com.couchbase.client.java.kv.ReplicateTo; /** * Tests for @Transactional methods, where parameters/options are being set that aren't support in a transaction. These * will be rejected at runtime. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(classes = { TransactionsConfig.class, diff --git a/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java index 94f5181f1..0e2b56dd4 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/DirectPlatformTransactionManagerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -31,12 +31,13 @@ /** * We do not support direct use of the PlatformTransactionManager. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) public class DirectPlatformTransactionManagerIntegrationTests extends JavaIntegrationTests { - @Autowired - CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseClientFactory couchbaseClientFactory; @Test public void directUseAlwaysFails() { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java index 23ce78c6d..efc5f05b4 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java @@ -1,14 +1,36 @@ +/* + * Copyright 2022 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.transactions; import java.util.UUID; -public class ObjectId{ - public ObjectId(){ - id = UUID.randomUUID().toString(); - } - String id; +/** + * ObjectId for Transaction tests + * + * @author Michael Reiche + */ +public class ObjectId { + public ObjectId() { + id = UUID.randomUUID().toString(); + } + + String id; - public String toString(){ - return id.toString(); - } + public String toString() { + return id.toString(); + } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java index 0d4b694a0..f7d0e43d5 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 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.transactions; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; @@ -20,6 +36,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; +/** + * PersonService for tests + * + * @author Michael Reiche + */ @Service @Component @EnableTransactionManagement @@ -73,7 +94,6 @@ public List saveWithLogs(Pe .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class) .withConsistency(REQUEST_PLUS).all()) // .as(transactionalOperator::transactional).collectList().block(); - } public List saveWithErrorLogs(Person person) { @@ -140,13 +160,14 @@ public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger * @param person * @return */ - //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // must use transactionalOperator public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { - //assertInAnnotationTransaction(true); + // assertInAnnotationTransaction(true); return personOperationsRx.findById(Person.class).one(person.id()) .map((p) -> ReplaceLoopThread.updateOutOfTransaction(personOperations, p, tryCount.incrementAndGet())) - .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))).as(transactionalOperator::transactional); + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))) + .as(transactionalOperator::transactional); } /** @@ -162,18 +183,19 @@ public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); } - //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // must use transactionalOperator public Mono declarativeSavePersonReactive(Person person) { // assertInAnnotationTransaction(true); return personOperationsRx.insertById(Person.class).one(person).as(transactionalOperator::transactional); } - //@Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + // @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // must use transactionalOperator public Mono declarativeSavePersonErrorsReactive(Person person) { - //assertInAnnotationTransaction(true); - return personOperationsRx.insertById(Person.class).one(person).map((pp) -> throwSimulateFailureException(pp)).as(transactionalOperator::transactional); // + // assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class).one(person).map((pp) -> throwSimulateFailureException(pp)) + .as(transactionalOperator::transactional); // } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java index d4b179c63..9442687be 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceReactive.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 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.transactions; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; @@ -12,7 +28,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; -// @RequiredArgsConstructor +/** + * reactive PersonService for tests + * + * @author Michael Reiche + */ class PersonServiceReactive { final ReactiveCouchbaseOperations personOperationsRx; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java index fc6b41029..b451a89b9 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReactiveTransactionalTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; -import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,6 +42,7 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.TransactionalSupport; import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -52,14 +52,14 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import com.couchbase.client.java.transactions.error.TransactionFailedException; - /** * Tests for reactive @Transactional, using the CouchbaseTransactionInterceptor. + * + * @author Graham Pople */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = { TransactionsConfig.class, - ReactiveTransactionalTemplateIntegrationTests.PersonService.class }) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, ReactiveTransactionalTemplateIntegrationTests.PersonService.class }) public class ReactiveTransactionalTemplateIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired PersonService personService; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java index 47895b7d2..361ac78e0 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/ReplaceLoopThread.java @@ -1,68 +1,85 @@ +/* + * Copyright 2022 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.transactions; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Assert; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.util.JavaIntegrationTests; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - +/** + * For testing transactions + * + * @author Michael Reiche + */ public class ReplaceLoopThread extends Thread { - private final CouchbaseOperations couchbaseOperations; - AtomicBoolean stop = new AtomicBoolean(false); - UUID id; - int maxIterations = 100; + private final CouchbaseOperations couchbaseOperations; + AtomicBoolean stop = new AtomicBoolean(false); + UUID id; + int maxIterations = 100; - public ReplaceLoopThread(CouchbaseOperations couchbaseOperations, UUID id, int... iterations) { - Assert.assertNotNull("couchbaseOperations cannot be null", couchbaseOperations); - this.couchbaseOperations = couchbaseOperations; - this.id = id; - if (iterations != null && iterations.length == 1) { - this.maxIterations = iterations[0]; - } - } + public ReplaceLoopThread(CouchbaseOperations couchbaseOperations, UUID id, int... iterations) { + Assert.assertNotNull("couchbaseOperations cannot be null", couchbaseOperations); + this.couchbaseOperations = couchbaseOperations; + this.id = id; + if (iterations != null && iterations.length == 1) { + this.maxIterations = iterations[0]; + } + } - public void run() { - for (int i = 0; i < maxIterations && !stop.get(); i++) { - JavaIntegrationTests.sleepMs(10); - try { - // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. - // annotations processed so we just check getFirstname().equals() - // switchedPerson has version=0, so it doesn't check CAS - Person fetched = couchbaseOperations.findById(Person.class).one(id.toString()); - couchbaseOperations.replaceById(Person.class).one(fetched.withFirstName("Changed externally")); - System.out.println("********** replace thread: " + i + " success"); - } catch (Exception e) { - System.out.println("********** replace thread: " + i + " " + e.getClass() - .getName()); - e.printStackTrace(); - } - } + public void run() { + for (int i = 0; i < maxIterations && !stop.get(); i++) { + JavaIntegrationTests.sleepMs(10); + try { + // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. + // annotations processed so we just check getFirstname().equals() + // switchedPerson has version=0, so it doesn't check CAS + Person fetched = couchbaseOperations.findById(Person.class).one(id.toString()); + couchbaseOperations.replaceById(Person.class).one(fetched.withFirstName("Changed externally")); + System.out.println("********** replace thread: " + i + " success"); + } catch (Exception e) { + System.out.println("********** replace thread: " + i + " " + e.getClass().getName()); + e.printStackTrace(); + } + } - } + } - public void setStopFlag() { - stop.set(true); - } + public void setStopFlag() { + stop.set(true); + } - public static Person updateOutOfTransaction(CouchbaseOperations couchbaseOperations, Person pp, int tryCount) { - System.err.println("updateOutOfTransaction: "+tryCount); - if (tryCount < 1) { - throw new RuntimeException("increment before calling updateOutOfTransactions"); - } - if (tryCount > 1) { - return pp; - } - ReplaceLoopThread t = new ReplaceLoopThread(couchbaseOperations, - pp.getId(), 1); - t.start(); - try { - t.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return pp; - } + public static Person updateOutOfTransaction(CouchbaseOperations couchbaseOperations, Person pp, int tryCount) { + System.err.println("updateOutOfTransaction: " + tryCount); + if (tryCount < 1) { + throw new RuntimeException("increment before calling updateOutOfTransactions"); + } + if (tryCount > 1) { + return pp; + } + ReplaceLoopThread t = new ReplaceLoopThread(couchbaseOperations, pp.getId(), 1); + t.start(); + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return pp; + } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java index 423cb3dc3..fdacc3045 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java @@ -1,15 +1,36 @@ +/* + * Copyright 2022 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.transactions; +/** + * A recognizable exception for testing transactions + * + * @author Michael Reiche + */ public class SimulateFailureException extends RuntimeException { - public SimulateFailureException(String... s){ - super(s!= null && s.length > 0 ? s[0] : null); - } + public SimulateFailureException(String... s) { + super(s != null && s.length > 0 ? s[0] : null); + } - public SimulateFailureException(){} + public SimulateFailureException() {} - public static void throwEx(String... s){ - throw new SimulateFailureException(s); - } + public static void throwEx(String... s) { + throw new SimulateFailureException(s); + } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java index bffca926b..07725cac9 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -58,6 +58,8 @@ /** * Tests for Spring's TransactionTemplate, used CouchbaseCallbackTransactionManager, using template methods (findById * etc.) + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java index 739dafd7d..268d6a5ba 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionsConfig.java @@ -1,6 +1,23 @@ +/* + * Copyright 2022 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.transactions; -import com.couchbase.client.java.env.ClusterEnvironment; +import java.time.Duration; + import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; @@ -8,9 +25,13 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.transaction.annotation.EnableTransactionManagement; -import java.time.Duration; - +import com.couchbase.client.java.env.ClusterEnvironment; +/** + * For testing transactions + * + * @author Michael Reiche + */ @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") @@ -40,7 +61,8 @@ public String getBucketName() { @Override public void configureEnvironment(ClusterEnvironment.Builder builder) { // twenty minutes for debugging in the debugger. - builder.transactionsConfig(com.couchbase.client.java.transactions.config.TransactionsConfig.builder().timeout(Duration.ofMinutes(20))); + builder.transactionsConfig( + com.couchbase.client.java.transactions.config.TransactionsConfig.builder().timeout(Duration.ofMinutes(20))); } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java index 744b203be..881dd4ef8 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -18,6 +18,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import reactor.core.publisher.Mono; + import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -40,14 +42,16 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.transactions.error.TransactionFailedException; -import reactor.core.publisher.Mono; /** - * Tests for regular reactive SDK transactions, where Spring operations that aren't supported in a transaction are being used. - * They should be prevented at runtime. + * Tests for regular reactive SDK transactions, where Spring operations that aren't supported in a transaction are being + * used. They should be prevented at runtime. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = {TransactionsConfig.class, SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.PersonService.class}) +@SpringJUnitConfig(classes = { TransactionsConfig.class, + SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.PersonService.class }) public class SDKReactiveTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -115,8 +119,7 @@ public void upsertById() { // This is intentionally not a @Transactional service @Service @Component - static - class PersonService { + static class PersonService { final ReactiveCouchbaseOperations personOperations; public PersonService(ReactiveCouchbaseOperations ops) { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java index c30ee137c..3f38ba720 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsPersonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -22,9 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.data.couchbase.transactions.ReplaceLoopThread.updateOutOfTransaction; -import org.springframework.data.couchbase.transactions.ReplaceLoopThread; -import org.springframework.data.couchbase.transactions.SimulateFailureException; -import org.springframework.data.couchbase.transactions.TransactionsConfig; import reactor.core.publisher.Mono; import java.util.LinkedList; @@ -48,6 +45,9 @@ import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; import org.springframework.data.couchbase.transactions.util.TransactionTestUtil; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -59,8 +59,10 @@ import com.couchbase.client.java.transactions.error.TransactionFailedException; /** - * Tests for ReactiveTransactionsWrapper, moved from CouchbasePersonTransactionIntegrationTests. - * Now ReactiveTransactionsWrapper is removed, these are testing the same operations inside a regular SDK transaction. + * Tests for ReactiveTransactionsWrapper, moved from CouchbasePersonTransactionIntegrationTests. Now + * ReactiveTransactionsWrapper is removed, these are testing the same operations inside a regular SDK transaction. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) @@ -90,15 +92,15 @@ public static void afterAll() { @BeforeEach public void beforeEachTest() { - WalterWhite = new Person( "Walter", "White"); + WalterWhite = new Person("Walter", "White"); TransactionTestUtil.assertNotInTransaction(); List rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List rp1 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List rp1 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); List p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List p1 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) - .all(); + List p1 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName) + .inCollection(cName).all(); } @AfterEach @@ -160,7 +162,9 @@ public void replacePersonCBTransactionsRxTmplRollback() { @Test public void deletePersonCBTransactionsRxTmpl() { Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get + // the + // ctx return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then(); }); result.block(); @@ -171,7 +175,9 @@ public void deletePersonCBTransactionsRxTmpl() { @Test // ok public void deletePersonCBTransactionsRxTmplFail() { Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get + // the + // ctx return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person) .then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)); }); @@ -183,7 +189,9 @@ public void deletePersonCBTransactionsRxTmplFail() { @Test public void deletePersonCBTransactionsRxRepo() { Person person = repo.withCollection(cName).save(WalterWhite); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get + // the + // ctx return rxRepo.withCollection(cName).delete(person).then(); }); result.block(); @@ -194,7 +202,9 @@ public void deletePersonCBTransactionsRxRepo() { @Test public void deletePersonCBTransactionsRxRepoFail() { Person person = repo.withCollection(cName).save(WalterWhite); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get + // the + // ctx return rxRepo.withCollection(cName).findById(person.id()) .flatMap(pp -> rxRepo.withCollection(cName).delete(pp).then(rxRepo.withCollection(cName).delete(pp))).then(); }); @@ -205,13 +215,12 @@ public void deletePersonCBTransactionsRxRepoFail() { @Test public void findPersonCBTransactions() { - Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName) - .one(WalterWhite); + Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite); List docs = new LinkedList<>(); Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - return rxCBTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName).matching(q) - .one().doOnSuccess(doc -> { + return rxCBTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName) + .matching(q).one().doOnSuccess(doc -> { System.err.println("doc: " + doc); docs.add(doc); }); @@ -252,8 +261,8 @@ public void findPersonSpringTransactions() { Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite); List docs = new LinkedList<>(); Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> rxCBTmpl.findByQuery(Person.class) - .inScope(sName).inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> rxCBTmpl + .findByQuery(Person.class).inScope(sName).inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); result.block(); assertFalse(docs.isEmpty(), "Should have found " + person); for (Object o : docs) { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java index 2a650858d..7b7842bff 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKReactiveTransactionsTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2022 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. @@ -23,13 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; -import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.transactions.ReplaceLoopThread; -import org.springframework.data.couchbase.transactions.SimulateFailureException; -import org.springframework.data.couchbase.transactions.TransactionsConfig; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; @@ -38,27 +31,35 @@ import java.util.function.Function; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.TransactionalSupport; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.client.java.transactions.config.TransactionOptions; import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * Tests using template methods (findById etc.) inside a regular reactive SDK transaction. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) @@ -71,8 +72,8 @@ public class SDKReactiveTransactionsTemplateIntegrationTests extends JavaIntegra Person WalterWhite; @BeforeEach - public void beforeEachTest(){ - WalterWhite = new Person ("Walter", "White"); + public void beforeEachTest() { + WalterWhite = new Person("Walter", "White"); } @AfterEach @@ -165,8 +166,8 @@ public void committedRemoveByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doInTransaction(ctx -> { - return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).matching(QueryCriteria.where("firstname").eq(WalterWhite.id())) - .all().then(); + return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS) + .matching(QueryCriteria.where("firstname").eq(WalterWhite.id())).all().then(); }); Person fetched = blocking.findById(Person.class).one(person.id()); @@ -180,7 +181,8 @@ public void committedFindByQuery() { Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname()); RunResult rr = doInTransaction(ctx -> { - return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.getFirstname())).all().then(); + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.getFirstname())) + .all().then(); }); assertEquals(1, rr.attempts); @@ -227,8 +229,7 @@ public void rollbackRemove() { assertThrowsWithCause(() -> doInTransaction(ctx -> { attempts.incrementAndGet(); - return ops.findById(Person.class).one(person.id()) - .flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // + return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).oneEntity(p)) // .doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result }), TransactionFailedException.class, SimulateFailureException.class); @@ -245,8 +246,8 @@ public void rollbackRemoveByQuery() { assertThrowsWithCause(() -> doInTransaction(ctx -> { attempts.incrementAndGet(); - return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all().elementAt(0) - .map(p -> throwSimulateFailureException(p)); + return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all() + .elementAt(0).map(p -> throwSimulateFailureException(p)); }), TransactionFailedException.class, SimulateFailureException.class); Person fetched = blocking.findById(Person.class).one(person.id()); @@ -262,8 +263,8 @@ public void rollbackFindByQuery() { assertThrowsWithCause(() -> doInTransaction(ctx -> { attempts.incrementAndGet(); - return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all().elementAt(0) - .map(p -> throwSimulateFailureException(p)); + return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all() + .elementAt(0).map(p -> throwSimulateFailureException(p)); }), TransactionFailedException.class, SimulateFailureException.class); assertEquals(1, attempts.get()); @@ -280,8 +281,8 @@ public void replacePerson() { assertNotEquals(person.getVersion(), refetched.getVersion()); assertThrowsWithCause(() -> doInTransaction(ctx -> ops.replaceById(Person.class).one(person), // old cas - TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2))), TransactionFailedException.class , - Exception.class ); + TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2))), TransactionFailedException.class, + Exception.class); } @@ -315,7 +316,8 @@ public void replaceEntityWithCasZero() { @DisplayName("Entity must have CAS field during remove") @Test public void removeEntityWithoutCas() { - PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class).one(new PersonWithoutVersion("Walter", "White")); + PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class) + .one(new PersonWithoutVersion("Walter", "White")); assertThrowsWithCause(() -> doInTransaction(ctx -> { return ops.findById(PersonWithoutVersion.class).one(person.id()) .flatMap(fetched -> ops.removeById(PersonWithoutVersion.class).oneEntity(fetched)); @@ -329,8 +331,7 @@ public void removeEntityById() { Person person = blocking.insertById(Person.class).one(WalterWhite); assertThrowsWithCause(() -> doInTransaction(ctx -> { - return ops.findById(Person.class).one(person.id()) - .flatMap(p -> ops.removeById(Person.class).one(p.id())); + return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).one(p.id())); }), TransactionFailedException.class, IllegalArgumentException.class); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java index a1f3005b6..c1a91a211 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsNonAllowableOperationsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -45,9 +45,12 @@ /** * Tests for regular SDK transactions, where Spring operations that aren't supported in a transaction are being used. * They should be prevented at runtime. + * + * @Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(classes = {TransactionsConfig.class, SDKTransactionsNonAllowableOperationsIntegrationTests.PersonService.class}) +@SpringJUnitConfig( + classes = { TransactionsConfig.class, SDKTransactionsNonAllowableOperationsIntegrationTests.PersonService.class }) public class SDKTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -116,8 +119,7 @@ public void upsertById() { // This is intentionally not a @Transactional service @Service @Component - static - class PersonService { + static class PersonService { final CouchbaseOperations personOperations; public PersonService(CouchbaseOperations ops) { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java index 5a2623f0b..9c96c2a0c 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/sdk/SDKTransactionsTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -24,15 +24,6 @@ import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction; import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction; -import com.couchbase.client.core.error.transaction.PreviousOperationFailedException; -import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; -import com.couchbase.client.java.json.JsonObject; -import com.couchbase.client.java.transactions.TransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionGetResult; -import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; -import org.springframework.data.couchbase.transactions.ReplaceLoopThread; -import org.springframework.data.couchbase.transactions.SimulateFailureException; -import org.springframework.data.couchbase.transactions.TransactionsConfig; import reactor.util.annotation.Nullable; import java.time.Duration; @@ -52,18 +43,29 @@ import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException; +import org.springframework.data.couchbase.transactions.ReplaceLoopThread; +import org.springframework.data.couchbase.transactions.SimulateFailureException; +import org.springframework.data.couchbase.transactions.TransactionsConfig; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.core.error.transaction.PreviousOperationFailedException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.client.java.transactions.config.TransactionOptions; import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * Tests for using template methods (findById etc.) inside a regular SDK transaction. + * + * @author Graham Pople */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(TransactionsConfig.class) @@ -99,8 +101,7 @@ private RunResult doInTransaction(Consumer lambda) { return doInTransaction(lambda, null); } - private RunResult doInTransaction(Consumer lambda, - @Nullable TransactionOptions options) { + private RunResult doInTransaction(Consumer lambda, @Nullable TransactionOptions options) { AtomicInteger attempts = new AtomicInteger(); TransactionResult result = couchbaseClientFactory.getCluster().transactions().run(ctx -> { @@ -367,8 +368,7 @@ public void casMismatchCausesRetry() { attempts.incrementAndGet()); try { ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction")); - } - catch (RuntimeException err) { + } catch (RuntimeException err) { assertTrue(err instanceof UncategorizedTransactionDataAccessException); } @@ -400,8 +400,7 @@ public void casMismatchUsingRegularTransactionOperations() { attempts.incrementAndGet()); try { ctx.replace(gr, JsonObject.create()); - } - catch (RuntimeException err) { + } catch (RuntimeException err) { assertTrue(err instanceof TransactionOperationFailedException); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java index 98dd2b691..83324cf53 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/util/TransactionTestUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2022 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. @@ -15,24 +15,24 @@ */ package org.springframework.data.couchbase.transactions.util; -import org.springframework.data.couchbase.core.TransactionalSupport; -import org.springframework.transaction.NoTransactionException; -import reactor.core.publisher.Mono; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.springframework.data.couchbase.core.TransactionalSupport; + /** * Utility methods for transaction tests. + * + * @author Graham Pople */ public class TransactionTestUtil { - private TransactionTestUtil() {} + private TransactionTestUtil() {} - public static void assertInTransaction() { - assertTrue(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); - } + public static void assertInTransaction() { + assertTrue(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); + } - public static void assertNotInTransaction() { - assertFalse(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); - } + public static void assertNotInTransaction() { + assertFalse(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()); + } } diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 0ee40cd71..2d1e6c9a5 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -27,13 +27,6 @@ import java.util.UUID; import java.util.stream.Collectors; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.core.service.Service; -import com.couchbase.client.java.ClusterOptions; -import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; -import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -53,10 +46,14 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions; import com.couchbase.client.java.manager.query.CreateQueryIndexOptions; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; /** * Parent class which drives all dynamic integration tests based on the configured cluster setup. * + * @author Michael Reiche + * * @since 2.0.0 */ @ExtendWith(ClusterInvocationProvider.class) diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index d294fd368..89b313202 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -27,12 +27,6 @@ import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; import static org.springframework.data.couchbase.util.Util.waitUntilCondition; -import okhttp3.Credentials; -import okhttp3.FormBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - import java.io.IOException; import java.time.Duration; import java.util.Collections; @@ -44,7 +38,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Timeout; @@ -58,6 +51,7 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.transactions.SimulateFailureException; import com.couchbase.client.core.diagnostics.PingResult; import com.couchbase.client.core.diagnostics.PingState; @@ -90,12 +84,11 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.search.SearchQuery; import com.couchbase.client.java.search.result.SearchResult; -import org.springframework.data.couchbase.transactions.SimulateFailureException; /** * Extends the {@link ClusterAwareIntegrationTests} with java-client specific code. * - * @Author Michael Reiche + * @author Michael Reiche */ // Temporarily increased timeout to (possibly) workaround MB-37011 when Developer Preview enabled @Timeout(value = 10, unit = TimeUnit.MINUTES) // Safety timer so tests can't block CI executors @@ -393,19 +386,18 @@ public static Throwable assertThrowsOneOf(Executable executable, Class... exp try { executable.execute(); - } - catch (Throwable actualException) { - for(Class expectedType:expectedTypes){ - if(actualException.getClass().isAssignableFrom( expectedType)){ + } catch (Throwable actualException) { + for (Class expectedType : expectedTypes) { + if (actualException.getClass().isAssignableFrom(expectedType)) { return actualException; } } UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); - String message = "Expected one of "+toString(expectedTypes)+" but was : "+actualException.getClass(); + String message = "Expected one of " + toString(expectedTypes) + " but was : " + actualException.getClass(); throw new AssertionFailedError(message, actualException); } - String message ="Expected one of "+toString(expectedTypes)+" to be thrown, but nothing was thrown."; + String message = "Expected one of " + toString(expectedTypes) + " to be thrown, but nothing was thrown."; throw new AssertionFailedError(message); } @@ -413,7 +405,7 @@ private static String toString(Object[] array) { StringBuffer sb = new StringBuffer(); sb.append("["); for (int i = 0; i < array.length; i++) { - if(i>0){ + if (i > 0) { sb.append(", "); } sb.append(array[i]); diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index 0ec98b390..74b1fe372 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. diff --git a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java index 21258c790..968c4b1c2 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java @@ -25,6 +25,8 @@ * tests for bootstrapping their own code. * * @since 2.0.0 + * + * @author Michael Reiche */ public class TestClusterConfig { diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 857787cb9..3a91d3718 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -146,25 +146,27 @@ public static Pair, List> comprisesNot(Iterable source, T[] al return Pair.of(unexpected, missing); } } - /** - * check if we are/are not in an @Transactional transaction - * @param inTransaction - */ - public static void assertInAnnotationTransaction(boolean inTransaction) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") - || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { - if (inTransaction) { - return; - } - } - } - if (!inTransaction) { - return; - } - throw new RuntimeException( - "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); - } + + /** + * check if we are/are not in an @Transactional transaction + * + * @param inTransaction + */ + public static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") + || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException("in-annotation-transaction = " + (!inTransaction) + + " but expected in-annotation-transaction = " + inTransaction); + } } diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 0c076c435..6efee2b37 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -22,7 +22,7 @@ - log additional debug info during automatic index creation --> - " + "