From 2cd928a0804ce042010fa24d5292bdb2149ea41e Mon Sep 17 00:00:00 2001 From: Tigran Babloyan Date: Fri, 3 Feb 2023 17:34:47 +0400 Subject: [PATCH] Added Sub-Document mutations support Closes #1659 --- src/main/asciidoc/template.adoc | 75 +++ .../core/CouchbaseExceptionTranslator.java | 8 +- .../couchbase/core/CouchbaseTemplate.java | 6 + .../core/ExecutableMutateInByIdOperation.java | 169 +++++ ...xecutableMutateInByIdOperationSupport.java | 188 ++++++ .../core/FluentCouchbaseOperations.java | 2 +- .../core/ReactiveCouchbaseTemplate.java | 6 + .../ReactiveFluentCouchbaseOperations.java | 2 +- .../core/ReactiveMutateInByIdOperation.java | 169 +++++ .../ReactiveMutateInByIdOperationSupport.java | 244 +++++++ .../couchbase/core/query/OptionsBuilder.java | 40 +- .../core/support/WithMutateInOptions.java | 33 + .../core/support/WithMutateInPaths.java | 32 + ...hbaseTemplateKeyValueIntegrationTests.java | 587 ++++++++++++++++- ...hbaseTemplateKeyValueIntegrationTests.java | 597 +++++++++++++++++- .../data/couchbase/domain/MutableUser.java | 28 + 16 files changed, 2168 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/MutableUser.java diff --git a/src/main/asciidoc/template.adoc b/src/main/asciidoc/template.adoc index f4d0705e6..bb9d47121 100644 --- a/src/main/asciidoc/template.adoc +++ b/src/main/asciidoc/template.adoc @@ -54,4 +54,79 @@ final List foundUsers = couchbaseTemplate .consistentWith(QueryScanConsistency.REQUEST_PLUS) .all(); ---- +==== + + +[[template.sub-document-ops]] +== Sub-Document Operations + +Couchbase supports https://docs.couchbase.com/java-sdk/current/howtos/subdocument-operations.html[Sub-Document Operations]. This section documents how to use it with Spring Data Couchbase. + + + +Sub-Document operations may be quicker and more network-efficient than full-document operations such as upsert or replace because they only transmit the accessed sections of the document over the network. + +Sub-Document operations are also atomic, in that if one Sub-Document mutation fails then all will, allowing safe modifications to documents with built-in concurrency control. + +Currently Spring Data Couchbase supports only sub document mutations (remove, upsert, replace and insert). + +Mutation operations modify one or more paths in the document. The simplest of these operations is upsert, which, similar to the fulldoc-level upsert, will either modify the value of an existing path or create it if it does not exist: + +Following example will upsert the city field on the address of the user, without trasfering any additional user document data. + +.MutateIn upsert on the template +==== +[source,java] +---- +User user = new User(); +// id field on the base document id required +user.setId(ID); +user.setAddress(address); +couchbaseTemplate.mutateInById(User.class) + .withUpsertPaths("address.city") + .one(user); +---- +==== + +[[template.sub-document-ops]] +=== Executing Multiple Sub-Document Operations + +Multiple Sub-Document operations can be executed at once on the same document, allowing you to modify several Sub-Documents at once. When multiple operations are submitted within the context of a single mutateIn command, the server will execute all the operations with the same version of the document. + +To execute several mutation operations the method chaining can be used. + +.MutateIn Multiple Operations +==== +[source,java] +---- +couchbaseTemplate.mutateInById(User.class) + .withInsertPaths("roles", "subuser.firstname") + .withRemovePaths("address.city") + .withUpsertPaths("firstname") + .withReplacePaths("address.street") + .one(user); +---- +==== + +[[template.sub-document-cas]] +=== Concurrent Modifications + +Concurrent Sub-Document operations on different parts of a document will not conflict so by default the CAS value will be not be supplied when executing the mutations. +If CAS is required then it can be provided like this: + +.MutateIn With CAS +==== +[source,java] +---- +User user = new User(); +// id field on the base document id required +user.setId(ID); +// @Version field should have a value for CAS to be supplied +user.setVersion(cas); +user.setAddress(address); +couchbaseTemplate.mutateInById(User.class) + .withUpsertPaths("address.city") + .withCasProvided() + .one(user); +---- ==== \ No newline at end of file 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 e0ba8af66..9fa0c7840 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.subdoc.*; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -63,6 +64,7 @@ * @author Simon Baslé * @author Michael Reiche * @author Graham Pople + * @author Tigran Babloyan */ public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator { @@ -102,7 +104,11 @@ public final DataAccessException translateExceptionIfPossible(final RuntimeExcep return new OperationCancellationException(ex.getMessage(), ex); } - if (ex instanceof DesignDocumentNotFoundException || ex instanceof ValueTooLargeException) { + if (ex instanceof DesignDocumentNotFoundException || ex instanceof ValueTooLargeException + || ex instanceof PathExistsException || ex instanceof PathInvalidException + || ex instanceof PathNotFoundException || ex instanceof PathMismatchException + || ex instanceof PathTooDeepException || ex instanceof ValueInvalidException + || ex instanceof ValueTooDeepException || ex instanceof DocumentTooDeepException) { return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); } 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 54579a88e..4bca6e330 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -41,6 +41,7 @@ * @author Michael Nitschinger * @author Michael Reiche * @author Jorge Rodriguez Martin + * @author Tigran Babloyan * @since 3.0 */ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContextAware { @@ -95,6 +96,11 @@ public ExecutableUpsertById upsertById(final Class domainType) { return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType); } + @Override + public ExecutableMutateInById mutateInById(Class domainType) { + return new ExecutableMutateInByIdOperationSupport(this).mutateInById(domainType); + } + @Override public ExecutableInsertById insertById(Class domainType) { return new ExecutableInsertByIdOperationSupport(this).insertById(domainType); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java new file mode 100644 index 000000000..208d65f03 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperation.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-2023 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.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.MutateInOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.support.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collection; + +/** + * Mutate In Operations + * + * @author Tigran Babloyan + * @since 5.1 + */ +public interface ExecutableMutateInByIdOperation { + + /** + * Mutate using the KV service. + * + * @param domainType the entity type to mutate. + */ + ExecutableMutateInById mutateInById(Class domainType); + + /** + * Terminating operations invoking the actual execution. + */ + interface TerminatingMutateInById extends OneAndAllEntity { + + /** + * Insert one entity. + * + * @return Inserted entity. + */ + @Override + T one(T object); + + /** + * Insert a collection of entities. + * + * @return Inserted entities + */ + @Override + Collection all(Collection objects); + + } + + interface MutateInByIdWithPaths extends TerminatingMutateInById, WithMutateInPaths { + /** + * Adds given paths to remove mutations. + * See {@link com.couchbase.client.java.kv.Remove} for more details. + * @param removePaths The property paths to removed from document. + */ + @Override + MutateInByIdWithPaths withRemovePaths(final String... removePaths); + /** + * Adds given paths to insert mutations. + * See {@link com.couchbase.client.java.kv.Insert} for more details. + * @param insertPaths The property paths to be inserted into the document. + */ + @Override + MutateInByIdWithPaths withInsertPaths(final String... insertPaths); + /** + * Adds given paths to upsert mutations. + * See {@link com.couchbase.client.java.kv.Upsert} for more details. + * @param upsertPaths The property paths to be upserted into the document. + */ + @Override + MutateInByIdWithPaths withUpsertPaths(final String... upsertPaths); + /** + * Adds given paths to replace mutations. + * See {@link com.couchbase.client.java.kv.Replace} for more details. + * @param replacePaths The property paths to be replaced in the document. + */ + @Override + MutateInByIdWithPaths withReplacePaths(final String... replacePaths); + /** + * Marks that the CAS value should be provided with the mutations to protect against concurrent modifications. + * By default the CAS value is not provided. + */ + MutateInByIdWithPaths withCasProvided(); + } + + /** + * Fluent method to specify options. + * + * @param the entity type to use. + */ + interface MutateInByIdWithOptions extends MutateInByIdWithPaths, WithMutateInOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + @Override + TerminatingMutateInById withOptions(MutateInOptions options); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface MutateInByIdInCollection extends MutateInByIdWithOptions, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + MutateInByIdWithOptions inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface MutateInByIdInScope extends MutateInByIdInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + MutateInByIdInCollection inScope(String scope); + } + + interface MutateInByIdWithDurability extends MutateInByIdInScope, WithDurability { + @Override + MutateInByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + MutateInByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface MutateInByIdWithExpiry extends MutateInByIdWithDurability, WithExpiry { + @Override + MutateInByIdWithDurability withExpiry(Duration expiry); + } + + /** + * Provides methods for constructing KV operations in a fluent way. + * + * @param the entity type to upsert + */ + interface ExecutableMutateInById extends MutateInByIdWithExpiry {} + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java new file mode 100644 index 000000000..36a5eed8d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012-2023 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.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.MutateInOptions; +import com.couchbase.client.java.kv.MutateInSpec; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.*; + +/** + * {@link ExecutableMutateInByIdOperation} implementations for Couchbase. + * + * @author Tigran Babloyan + */ +public class ExecutableMutateInByIdOperationSupport implements ExecutableMutateInByIdOperation { + + private final CouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ExecutableMutateInByIdOperationSupport.class); + + public ExecutableMutateInByIdOperationSupport(final CouchbaseTemplate template) { + this.template = template; + } + + @Override + public ExecutableMutateInById mutateInById(final Class domainType) { + Assert.notNull(domainType, "DomainType must not be null!"); + return new ExecutableMutateInByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), false); + } + + static class ExecutableMutateInByIdSupport implements ExecutableMutateInById { + + private final CouchbaseTemplate template; + private final Class domainType; + private final String scope; + private final String collection; + private final MutateInOptions options; + private final PersistTo persistTo; + private final ReplicateTo replicateTo; + private final DurabilityLevel durabilityLevel; + private final Duration expiry; + private final boolean provideCas; + private final List removePaths = new ArrayList<>(); + private final List upsertPaths = new ArrayList<>(); + private final List insertPaths = new ArrayList<>(); + private final List replacePaths = new ArrayList<>(); + private final ReactiveMutateInByIdOperationSupport.ReactiveMutateInByIdSupport reactiveSupport; + + + ExecutableMutateInByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final MutateInOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, final List removePaths, + final List upsertPaths, final List insertPaths, final List replacePaths, final boolean provideCas) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.persistTo = persistTo; + this.replicateTo = replicateTo; + this.durabilityLevel = durabilityLevel; + this.expiry = expiry; + this.removePaths.addAll(removePaths); + this.upsertPaths.addAll(upsertPaths); + this.insertPaths.addAll(insertPaths); + this.replacePaths.addAll(replacePaths); + this.provideCas = provideCas; + this.reactiveSupport = new ReactiveMutateInByIdOperationSupport.ReactiveMutateInByIdSupport(template.reactive(), domainType, scope, collection, + options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support()), removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public T one(final T object) { + return reactiveSupport.one(object).block(); + } + + @Override + public Collection all(Collection objects) { + return reactiveSupport.all(objects).collectList().block(); + } + + @Override + public TerminatingMutateInById withOptions(final MutateInOptions options) { + Assert.notNull(options, "Options must not be null."); + return new ExecutableMutateInByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability inCollection(final String collection) { + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, + collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry, + removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdInCollection inScope(final String scope) { + return new ExecutableMutateInByIdSupport<>(template, domainType, scope != null ? scope : this.scope, collection, + options, persistTo, replicateTo, durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, + replacePaths, provideCas); + } + + @Override + public MutateInByIdInScope withDurability(final DurabilityLevel durabilityLevel) { + Assert.notNull(durabilityLevel, "Durability Level must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdInScope withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { + Assert.notNull(persistTo, "PersistTo must not be null."); + Assert.notNull(replicateTo, "ReplicateTo must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withExpiry(final Duration expiry) { + Assert.notNull(expiry, "expiry must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withRemovePaths(final String... removePaths) { + Assert.notNull(removePaths, "removePaths path must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, Arrays.asList(removePaths), upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withUpsertPaths(final String... upsertPaths) { + Assert.notNull(upsertPaths, "upsertPaths path must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, Arrays.asList(upsertPaths), insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withInsertPaths(final String... insertPaths) { + Assert.notNull(insertPaths, "insertPaths path must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, Arrays.asList(insertPaths), replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withReplacePaths(final String... replacePaths) { + Assert.notNull(replacePaths, "replacePaths path must not be null."); + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, Arrays.asList(replacePaths), provideCas); + } + + @Override + public MutateInByIdWithPaths withCasProvided() { + return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, true); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java index a93db3f90..eee5dc0bd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/FluentCouchbaseOperations.java @@ -22,4 +22,4 @@ public interface FluentCouchbaseOperations extends ExecutableUpsertByIdOperation, ExecutableInsertByIdOperation, ExecutableReplaceByIdOperation, ExecutableFindByIdOperation, ExecutableFindFromReplicasByIdOperation, ExecutableFindByQueryOperation, ExecutableFindByAnalyticsOperation, ExecutableExistsByIdOperation, - ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation {} + ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation, ExecutableMutateInByIdOperation {} 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 b5d7c341e..3a032586b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -44,6 +44,7 @@ * @author Michael Reiche * @author Jorge Rodriguez Martin * @author Carlos Espinaco + * @author Tigran Babloyan */ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware { @@ -183,6 +184,11 @@ public ReactiveUpsertById upsertById(Class domainType) { return new ReactiveUpsertByIdOperationSupport(this).upsertById(domainType); } + @Override + public ReactiveMutateInById mutateInById(Class domainType) { + return new ReactiveMutateInByIdOperationSupport(this).mutateInById(domainType); + } + @Override public String getBucketName() { return clientFactory.getBucket().name(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java index d6e1c0ba6..d7652642c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFluentCouchbaseOperations.java @@ -22,4 +22,4 @@ public interface ReactiveFluentCouchbaseOperations extends ReactiveUpsertByIdOperation, ReactiveInsertByIdOperation, ReactiveReplaceByIdOperation, ReactiveFindByIdOperation, ReactiveExistsByIdOperation, ReactiveFindByAnalyticsOperation, ReactiveFindFromReplicasByIdOperation, ReactiveFindByQueryOperation, - ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation {} + ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation, ReactiveMutateInByIdOperation {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java new file mode 100644 index 000000000..7be0520be --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperation.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-2023 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.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.MutateInOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.support.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collection; + +/** + * Mutate In Operations + * + * @author Tigran Babloyan + * @since 5.1 + */ +public interface ReactiveMutateInByIdOperation { + + /** + * Mutate using the KV service. + * + * @param domainType the entity type to mutate. + */ + ReactiveMutateInById mutateInById(Class domainType); + + /** + * Terminating operations invoking the actual execution. + */ + interface TerminatingMutateInById extends OneAndAllEntityReactive { + + /** + * Mutate one entity. + * + * @return Upserted entity. + */ + @Override + Mono one(T object); + + /** + * Mutate a collection of entities. + * + * @return Inserted entities + */ + @Override + Flux all(Collection objects); + + } + + interface MutateInByIdWithPaths extends TerminatingMutateInById, WithMutateInPaths { + /** + * Adds given paths to remove mutations. + * See {@link com.couchbase.client.java.kv.Remove} for more details. + * @param removePaths The property paths to removed from document. + */ + @Override + MutateInByIdWithPaths withRemovePaths(final String... removePaths); + /** + * Adds given paths to insert mutations. + * See {@link com.couchbase.client.java.kv.Insert} for more details. + * @param insertPaths The property paths to be inserted into the document. + */ + @Override + MutateInByIdWithPaths withInsertPaths(final String... insertPaths); + /** + * Adds given paths to upsert mutations. + * See {@link com.couchbase.client.java.kv.Upsert} for more details. + * @param upsertPaths The property paths to be upserted into the document. + */ + @Override + MutateInByIdWithPaths withUpsertPaths(final String... upsertPaths); + /** + * Adds given paths to replace mutations. + * See {@link com.couchbase.client.java.kv.Replace} for more details. + * @param replacePaths The property paths to be replaced in the document. + */ + @Override + MutateInByIdWithPaths withReplacePaths(final String... replacePaths); + /** + * Marks that the CAS value should be provided with the mutations to protect against concurrent modifications. + * By default the CAS value is not provided. + */ + MutateInByIdWithPaths withCasProvided(); + } + + /** + * Fluent method to specify options. + * + * @param the entity type to use. + */ + interface MutateInByIdWithOptions extends MutateInByIdWithPaths, WithMutateInOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + @Override + TerminatingMutateInById withOptions(MutateInOptions options); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface MutateInByIdInCollection extends MutateInByIdWithOptions, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + MutateInByIdWithOptions inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface MutateInByIdInScope extends MutateInByIdInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + MutateInByIdInCollection inScope(String scope); + } + + interface MutateInByIdWithDurability extends MutateInByIdInScope, WithDurability { + @Override + MutateInByIdInScope withDurability(DurabilityLevel durabilityLevel); + + @Override + MutateInByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface MutateInByIdWithExpiry extends MutateInByIdWithDurability, WithExpiry { + @Override + MutateInByIdWithDurability withExpiry(Duration expiry); + } + + /** + * Provides methods for constructing KV operations in a fluent way. + * + * @param the entity type to upsert + */ + interface ReactiveMutateInById extends MutateInByIdWithExpiry {} + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java new file mode 100644 index 000000000..f44c871f3 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java @@ -0,0 +1,244 @@ +/* + * Copyright 2012-2023 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.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.MutateInOptions; +import com.couchbase.client.java.kv.MutateInSpec; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseList; +import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.*; + +/** + * {@link ReactiveMutateInByIdOperation} implementations for Couchbase. + * + * @author Tigran Babloyan + */ +public class ReactiveMutateInByIdOperationSupport implements ReactiveMutateInByIdOperation { + + private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveMutateInByIdOperationSupport.class); + + public ReactiveMutateInByIdOperationSupport(final ReactiveCouchbaseTemplate template) { + this.template = template; + } + + @Override + public ReactiveMutateInById mutateInById(final Class domainType) { + Assert.notNull(domainType, "DomainType must not be null!"); + return new ReactiveMutateInByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), + null, template.support(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), false); + } + + static class ReactiveMutateInByIdSupport implements ReactiveMutateInById { + + private final ReactiveCouchbaseTemplate template; + private final Class domainType; + private final String scope; + private final String collection; + private final MutateInOptions options; + private final PersistTo persistTo; + private final ReplicateTo replicateTo; + private final DurabilityLevel durabilityLevel; + private final Duration expiry; + private final ReactiveTemplateSupport support; + private final boolean provideCas; + private final List removePaths = new ArrayList<>(); + private final List upsertPaths = new ArrayList<>(); + private final List insertPaths = new ArrayList<>(); + private final List replacePaths = new ArrayList<>(); + + + ReactiveMutateInByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final MutateInOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support, final List removePaths, + final List upsertPaths, final List insertPaths, final List replacePaths, final boolean provideCas) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.persistTo = persistTo; + this.replicateTo = replicateTo; + this.durabilityLevel = durabilityLevel; + this.expiry = expiry; + this.support = support; + this.removePaths.addAll(removePaths); + this.upsertPaths.addAll(upsertPaths); + this.insertPaths.addAll(insertPaths); + this.replacePaths.addAll(replacePaths); + this.provideCas = provideCas; + } + + @Override + public Mono one(T object) { + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + if (LOG.isDebugEnabled()) { + LOG.debug("upsertById object={} {}", object, pArgs); + } + + Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction("mutateInById") + .then(support.encodeEntity(object)).flatMap(converted -> { + return Mono + .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection())) + .flatMap(collection -> collection.reactive() + .mutateIn(converted.getId().toString(), getMutations(converted), buildMutateInOptions(pArgs.getOptions(), object, converted)) + .flatMap( + result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null))); + }); + + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } + + @Override + public Flux all(Collection objects) { + return Flux.fromIterable(objects).flatMap(this::one); + } + + @Override + public TerminatingMutateInById withOptions(final MutateInOptions options) { + Assert.notNull(options, "Options must not be null."); + return new ReactiveMutateInByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability inCollection(final String collection) { + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, + collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, expiry, + support, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdInCollection inScope(final String scope) { + return new ReactiveMutateInByIdSupport<>(template, domainType, scope != null ? scope : this.scope, collection, + options, persistTo, replicateTo, durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, + replacePaths, provideCas); + } + + @Override + public MutateInByIdInScope withDurability(final DurabilityLevel durabilityLevel) { + Assert.notNull(durabilityLevel, "Durability Level must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdInScope withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { + Assert.notNull(persistTo, "PersistTo must not be null."); + Assert.notNull(replicateTo, "ReplicateTo must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withExpiry(final Duration expiry) { + Assert.notNull(expiry, "expiry must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withRemovePaths(final String... removePaths) { + Assert.notNull(removePaths, "removePaths path must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, Arrays.asList(removePaths), upsertPaths, insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withUpsertPaths(final String... upsertPaths) { + Assert.notNull(upsertPaths, "upsertPaths path must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, Arrays.asList(upsertPaths), insertPaths, replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withInsertPaths(final String... insertPaths) { + Assert.notNull(insertPaths, "insertPaths path must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, Arrays.asList(insertPaths), replacePaths, provideCas); + } + + @Override + public MutateInByIdWithDurability withReplacePaths(final String... replacePaths) { + Assert.notNull(replacePaths, "replacePaths path must not be null."); + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, Arrays.asList(replacePaths), provideCas); + } + + @Override + public MutateInByIdWithPaths withCasProvided() { + return new ReactiveMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, support, removePaths, upsertPaths, insertPaths, replacePaths, true); + } + + private MutateInOptions buildMutateInOptions(MutateInOptions options, T object, CouchbaseDocument doc) { + return OptionsBuilder.buildMutateInOptions(options, persistTo, replicateTo, durabilityLevel, expiry, doc, + provideCas ? support.getCas(object) : null); + } + + private List getMutations(CouchbaseDocument document) { + List mutations = new ArrayList<>(); + for (String path : removePaths) { + mutations.add(MutateInSpec.remove(path)); + } + for (String path : upsertPaths) { + mutations.add(MutateInSpec.upsert(path, getCouchbaseContent(document, path)).createPath()); + } + for (String path : insertPaths) { + mutations.add(MutateInSpec.insert(path, getCouchbaseContent(document, path)).createPath()); + } + for (String path : replacePaths) { + mutations.add(MutateInSpec.replace(path, getCouchbaseContent(document, path))); + } + return mutations; + } + + private Object getCouchbaseContent(CouchbaseDocument document, String path) { + Object result = document.export(); + for(var node : path.split("\\.")) { + if(result instanceof Map map) { + result = map.get(node); + } else { + throw new IllegalArgumentException("Path " + path + " is not valid."); + } + } + return result; + } + } + +} 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 308a3f5d5..c3a0e2cf0 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 @@ -32,7 +32,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.core.mapping.Expiry; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.couchbase.repository.Scope; @@ -46,6 +45,7 @@ import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.MutateInOptions; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; @@ -163,6 +163,28 @@ public static UpsertOptions buildUpsertOptions(UpsertOptions options, PersistTo return options; } + public static MutateInOptions buildMutateInOptions(MutateInOptions options, PersistTo persistTo, ReplicateTo replicateTo, + DurabilityLevel durabilityLevel, Duration expiry, CouchbaseDocument doc, Long cas) { + options = options != null ? options : MutateInOptions.mutateInOptions(); + if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { + options.durability(persistTo, replicateTo); + } else if (durabilityLevel != DurabilityLevel.NONE) { + options.durability(durabilityLevel); + } + if (expiry != null) { + options.expiry(expiry); + } else if (doc.getExpiration() != 0) { + options.expiry(Duration.ofSeconds(doc.getExpiration())); + } + if (cas != null) { + options.cas(cas); + } + if (LOG.isDebugEnabled()) { + LOG.debug("mutate in options: {}" + toString(options)); + } + return options; + } + public static ReplaceOptions buildReplaceOptions(ReplaceOptions options, PersistTo persistTo, ReplicateTo replicateTo, DurabilityLevel durabilityLevel, Duration expiry, Long cas, CouchbaseDocument doc) { options = options != null ? options : ReplaceOptions.replaceOptions(); @@ -332,6 +354,22 @@ static String toString(RemoveOptions o) { return s.toString(); } + static String toString(MutateInOptions o) { + StringBuilder s = new StringBuilder(); + MutateInOptions.Built b = o.build(); + s.append("{"); + s.append("cas: " + b.cas()); + s.append(", durabilityLevel: " + b.durabilityLevel()); + s.append(", persistTo: " + b.persistTo()); + s.append(", replicateTo: " + b.replicateTo()); + s.append(", timeout: " + b.timeout()); + s.append(", retryStrategy: " + b.retryStrategy()); + s.append(", clientContext: " + b.clientContext()); + s.append(", parentSpan: " + b.parentSpan()); + s.append("}"); + return s.toString(); + } + private static JsonObject getQueryOpts(QueryOptions.Built optsBuilt) { JsonObject jo = JsonObject.create(); optsBuilt.injectParams(jo); diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java new file mode 100644 index 000000000..ffd39229f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInOptions.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021-2023 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.kv.MutateInOptions; + +/** + * A common interface for all of Insert, Replace, Upsert mutations that take options. + * + * @author Tigran Babloyan + * @param - the entity class + */ +public interface WithMutateInOptions { + /** + * Specify options + * + * @param options The mutate options to use. + */ + Object withOptions(MutateInOptions options); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java new file mode 100644 index 000000000..90c53b6d5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithMutateInPaths.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2023 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; + +/** + * A common interface for all of Insert, Replace, Upsert and Remove mutations that take options. + * + * @author Tigran Babloyan + * @param - the entity class + */ +public interface WithMutateInPaths { + Object withRemovePaths(final String... removePaths); + + Object withInsertPaths(final String... insertPaths); + + Object withReplacePaths(final String... replacePaths); + + Object withUpsertPaths(final String... upsertPaths); +} 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 7c5e68923..e78365746 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -27,20 +27,16 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.UUID; +import java.util.*; import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.core.retry.RetryReason; 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.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById; import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; @@ -460,6 +456,583 @@ void upsertAndRemoveById() { .one(modified.getId()); } } + + @Test + void mutateInByIdUpsert(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + couchbaseTemplate.mutateInById(MutableUser.class).withUpsertPaths("roles").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + mutableUser.setVersion(999); + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class) + .withCasProvided().withUpsertPaths("roles").one(mutableUser)); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + couchbaseTemplate.mutateInById(MutableUser.class) + .withCasProvided().withUpsertPaths("roles").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + couchbaseTemplate.mutateInById(MutableUser.class).withUpsertPaths("roles", "address.street").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + couchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY).withUpsertPaths("roles", "address.street").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + couchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)).withUpsertPaths("roles", "address.street").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplace(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + couchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("firstname").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setVersion(999); + + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withReplacePaths("firstname").one(mutableUser)); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withReplacePaths("firstname").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableUser.setAddress(mutableAddress); + couchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("firstname", "address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableUser.setAddress(mutableAddress); + couchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY).withReplacePaths("firstname", "address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + user.setAddress(address); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + couchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)).withReplacePaths("firstname", "address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + + assertThrows(InvalidDataAccessResourceUsageException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("roles", "address.city").one(mutableUser)); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsert(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + couchbaseTemplate.mutateInById(MutableUser.class).withInsertPaths("roles").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertNotEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + mutableUser.setVersion(999); + + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withInsertPaths("roles").one(mutableUser)); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withInsertPaths("roles").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + couchbaseTemplate.mutateInById(MutableUser.class).withInsertPaths("roles", "address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + couchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withInsertPaths("address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + + couchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withInsertPaths("address.city").one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + + assertThrows(InvalidDataAccessResourceUsageException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class).withInsertPaths("firstname").one(mutableUser)); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemove(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + assertNotNull(user.getRoles()); + + couchbaseTemplate.mutateInById(MutableUser.class).withRemovePaths("roles").one(user); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertNull(user.getRoles()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + String userId = user.getId(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", "otherlast"); + mutableUser.setVersion(999); + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withRemovePaths("roles").one(mutableUser)); + + // success on correct cas + user.setVersion(user.getVersion()); + couchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withRemovePaths("roles").one(user); + + user = couchbaseTemplate.findById(MutableUser.class).one(userId); + + assertNull(user.getRoles()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + user.setAddress(mutableAddress); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + couchbaseTemplate.mutateInById(MutableUser.class).withRemovePaths("roles", "address.city").one(user); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertNull(user.getRoles()); + assertNull(user.getAddress().getCity()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + couchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withRemovePaths("lastname").one(user); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertNull(user.getLastname()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + couchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withRemovePaths("lastname").one(user); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + assertNull(user.getLastname()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + + assertThrows(InvalidDataAccessResourceUsageException.class, ()-> couchbaseTemplate.mutateInById(MutableUser.class) + .withRemovePaths("roles").one(mutableUser)); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdChained(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = couchbaseTemplate.insertById(MutableUser.class).one(user); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mAddress = new Address(); + mAddress.setCity("othercity"); + mAddress.setStreet("otherstreet"); + mutableUser.setAddress(mAddress); + mutableUser.setSubuser(new MutableUser("subuser", "subfirstname", "sublastname")); + + couchbaseTemplate.mutateInById(MutableUser.class) + .withInsertPaths("roles", "subuser.firstname") + .withRemovePaths("address.city") + .withUpsertPaths("firstname") + .withReplacePaths("address.street") + .one(mutableUser); + + user = couchbaseTemplate.findById(MutableUser.class).one(user.getId()); + + assertNull(user.getAddress().getCity()); + assertEquals(mAddress.getStreet(), user.getAddress().getStreet()); + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(mutableUser.getSubuser().getFirstname(), user.getSubuser().getFirstname()); + assertNull(user.getSubuser().getLastname()); + } finally { + couchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } @Test void insertById() { 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 5767d9d8a..e1aa3e349 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -28,20 +28,17 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.UUID; +import java.util.*; import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.core.retry.RetryReason; +import com.couchbase.client.java.query.QueryScanConsistency; 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.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ReactiveFindByIdOperation.ReactiveFindById; import org.springframework.data.couchbase.core.ReactiveRemoveByIdOperation.ReactiveRemoveById; @@ -341,6 +338,592 @@ void upsertAndRemoveById() { } } + @Test + void mutateInByIdUpsert(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withUpsertPaths("roles").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + mutableUser.setVersion(999); + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class) + .withCasProvided().withUpsertPaths("roles").one(mutableUser).block()); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class) + .withCasProvided().withUpsertPaths("roles").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withUpsertPaths("roles", "address.street").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withUpsertPaths("roles", "address.street").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdUpsertMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + mutableUser.setAddress(address); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withUpsertPaths("roles", "address.street").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getRoles(), mutableUser.getRoles()); + assertEquals(user.getAddress().getStreet(), mutableUser.getAddress().getStreet()); + assertNull(user.getAddress().getCity()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplace(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("firstname").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setVersion(999); + + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withReplacePaths("firstname").one(mutableUser).block()); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withReplacePaths("firstname").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableUser.setAddress(mutableAddress); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("firstname", "address.city") + .one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableUser.setAddress(mutableAddress); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withReplacePaths("firstname", "address.city").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + user.setAddress(address); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withReplacePaths("firstname", "address.city").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdReplaceMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + + assertThrows(InvalidDataAccessResourceUsageException.class, + ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withReplacePaths("roles", "address.city") + .one(mutableUser).block()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsert(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withInsertPaths("roles").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertNotEquals(mutableUser.getFirstname(), user.getFirstname()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + mutableUser.setVersion(999); + + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withInsertPaths("roles").one(mutableUser).block()); + + // success on correct cas + mutableUser.setVersion(user.getVersion()); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withInsertPaths("roles").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withInsertPaths("roles", "address.city").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withInsertPaths("address.city").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + mutableUser.setAddress(mutableAddress); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withInsertPaths("address.city").one(mutableUser).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertEquals(user.getAddress().getCity(), mutableUser.getAddress().getCity()); + assertNull(user.getAddress().getStreet()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdInsertMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + + assertThrows(InvalidDataAccessResourceUsageException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class) + .withInsertPaths("firstname").one(mutableUser).block()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemove(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + assertNotNull(user.getRoles()); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withRemovePaths("roles").one(user).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertNull(user.getRoles()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveWithCAS(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + String userId = user.getId(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", "otherlast"); + mutableUser.setVersion(999); + // if case is incorrect then exception is thrown + assertThrows(OptimisticLockingFailureException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided() + .withRemovePaths("roles").one(mutableUser).block()); + + // success on correct cas + user.setVersion(user.getVersion()); + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withCasProvided().withRemovePaths("roles").one(user).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(userId).block(); + + assertNull(user.getRoles()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultiple() { + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user.setRoles(Collections.singletonList("ADMIN")); + Address mutableAddress = new Address(); + mutableAddress.setCity("othercity"); + mutableAddress.setStreet("street"); + user.setAddress(mutableAddress); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withRemovePaths("roles", "address.city").one(user).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertNull(user.getRoles()); + assertNull(user.getAddress().getCity()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultipleWithDurability(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withDurability(DurabilityLevel.MAJORITY) + .withRemovePaths("lastname").one(user).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertNull(user.getLastname()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMultipleWithExpiry(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class).withExpiry(Duration.ofSeconds(1)) + .withRemovePaths("lastname").one(user).block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + assertNull(user.getLastname()); + + // user should be gone + int tries = 0; + MutableUser foundUser; + do { + sleepSecs(2); + foundUser = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + } while (tries++ < 5 && foundUser != null); + assertNull(foundUser); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdRemoveMissingPath(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + + assertThrows(InvalidDataAccessResourceUsageException.class, ()-> reactiveCouchbaseTemplate.mutateInById(MutableUser.class) + .withRemovePaths("roles").one(mutableUser).block()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void mutateInByIdChained(){ + try { + MutableUser user = new MutableUser(UUID.randomUUID().toString(), "firstname", "lastname"); + Address address = new Address(); + address.setCity("city"); + address.setStreet("street"); + user.setAddress(address); + user = reactiveCouchbaseTemplate.insertById(MutableUser.class).one(user).block(); + + MutableUser mutableUser = new MutableUser(user.getId(), "othername", ""); + mutableUser.setRoles(Collections.singletonList("ADMIN")); + Address mAddress = new Address(); + mAddress.setCity("othercity"); + mAddress.setStreet("otherstreet"); + mutableUser.setAddress(mAddress); + mutableUser.setSubuser(new MutableUser("subuser", "subfirstname", "sublastname")); + + reactiveCouchbaseTemplate.mutateInById(MutableUser.class) + .withInsertPaths("roles", "subuser.firstname") + .withRemovePaths("address.city") + .withUpsertPaths("firstname") + .withReplacePaths("address.street") + .one(mutableUser) + .block(); + + user = reactiveCouchbaseTemplate.findById(MutableUser.class).one(user.getId()).block(); + + assertNull(user.getAddress().getCity()); + assertEquals(mAddress.getStreet(), user.getAddress().getStreet()); + assertEquals(mutableUser.getRoles(), user.getRoles()); + assertEquals(mutableUser.getFirstname(), user.getFirstname()); + assertEquals(mutableUser.getSubuser().getFirstname(), user.getSubuser().getFirstname()); + assertNull(user.getSubuser().getLastname()); + } finally { + reactiveCouchbaseTemplate.removeByQuery(MutableUser.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + @Test void insertById() { User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); diff --git a/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java b/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java new file mode 100644 index 000000000..ffaee64f7 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/MutableUser.java @@ -0,0 +1,28 @@ +package org.springframework.data.couchbase.domain; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.util.List; + +@Document +public class MutableUser extends User{ + public MutableUser(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } + + @Getter + @Setter + private Address address; + + @Getter + @Setter + private MutableUser subuser; + + @Getter + @Setter + private List roles; + + +}