Added Sub-Document mutations support (#1660)

Closes #1659
This commit is contained in:
Tigran Babloyan
2023-02-03 23:26:59 +04:00
committed by GitHub
parent 4fa6eb43d2
commit 020693e995
16 changed files with 2168 additions and 18 deletions

View File

@@ -54,4 +54,79 @@ final List<User> 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);
----
====

View File

@@ -19,6 +19,7 @@ package org.springframework.data.couchbase.core;
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 @@ import com.couchbase.client.core.error.transaction.TransactionOperationFailedExc
* @author Simon Baslé
* @author Michael Reiche
* @author Graham Pople
* @author Tigran Babloyan
*/
public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator {
@@ -102,7 +104,11 @@ public class CouchbaseExceptionTranslator implements PersistenceExceptionTransla
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);
}

View File

@@ -41,6 +41,7 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @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 class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex
return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType);
}
@Override
public <T> ExecutableMutateInById<T> mutateInById(Class<T> domainType) {
return new ExecutableMutateInByIdOperationSupport(this).mutateInById(domainType);
}
@Override
public <T> ExecutableInsertById<T> insertById(Class<T> domainType) {
return new ExecutableInsertByIdOperationSupport(this).insertById(domainType);

View File

@@ -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.
*/
<T> ExecutableMutateInById<T> mutateInById(Class<T> domainType);
/**
* Terminating operations invoking the actual execution.
*/
interface TerminatingMutateInById<T> extends OneAndAllEntity<T> {
/**
* Insert one entity.
*
* @return Inserted entity.
*/
@Override
T one(T object);
/**
* Insert a collection of entities.
*
* @return Inserted entities
*/
@Override
Collection<? extends T> all(Collection<? extends T> objects);
}
interface MutateInByIdWithPaths<T> extends TerminatingMutateInById<T>, WithMutateInPaths<T> {
/**
* 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<T> 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<T> 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<T> 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<T> 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<T> withCasProvided();
}
/**
* Fluent method to specify options.
*
* @param <T> the entity type to use.
*/
interface MutateInByIdWithOptions<T> extends MutateInByIdWithPaths<T>, WithMutateInOptions<T> {
/**
* Fluent method to specify options to use for execution
*
* @param options to use for execution
*/
@Override
TerminatingMutateInById<T> withOptions(MutateInOptions options);
}
/**
* Fluent method to specify the collection.
*
* @param <T> the entity type to use for the results.
*/
interface MutateInByIdInCollection<T> extends MutateInByIdWithOptions<T>, InCollection<Object> {
/**
* With a different collection
*
* @param collection the collection to use.
*/
@Override
MutateInByIdWithOptions<T> inCollection(String collection);
}
/**
* Fluent method to specify the scope.
*
* @param <T> the entity type to use for the results.
*/
interface MutateInByIdInScope<T> extends MutateInByIdInCollection<T>, InScope<Object> {
/**
* With a different scope
*
* @param scope the scope to use.
*/
@Override
MutateInByIdInCollection<T> inScope(String scope);
}
interface MutateInByIdWithDurability<T> extends MutateInByIdInScope<T>, WithDurability<T> {
@Override
MutateInByIdInScope<T> withDurability(DurabilityLevel durabilityLevel);
@Override
MutateInByIdInScope<T> withDurability(PersistTo persistTo, ReplicateTo replicateTo);
}
interface MutateInByIdWithExpiry<T> extends MutateInByIdWithDurability<T>, WithExpiry<T> {
@Override
MutateInByIdWithDurability<T> withExpiry(Duration expiry);
}
/**
* Provides methods for constructing KV operations in a fluent way.
*
* @param <T> the entity type to upsert
*/
interface ExecutableMutateInById<T> extends MutateInByIdWithExpiry<T> {}
}

View File

@@ -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 <T> ExecutableMutateInById<T> mutateInById(final Class<T> 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<T> implements ExecutableMutateInById<T> {
private final CouchbaseTemplate template;
private final Class<T> 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<String> removePaths = new ArrayList<>();
private final List<String> upsertPaths = new ArrayList<>();
private final List<String> insertPaths = new ArrayList<>();
private final List<String> replacePaths = new ArrayList<>();
private final ReactiveMutateInByIdOperationSupport.ReactiveMutateInByIdSupport<T> reactiveSupport;
ExecutableMutateInByIdSupport(final CouchbaseTemplate template, final Class<T> domainType, final String scope,
final String collection, final MutateInOptions options, final PersistTo persistTo, final ReplicateTo replicateTo,
final DurabilityLevel durabilityLevel, final Duration expiry, final List<String> removePaths,
final List<String> upsertPaths, final List<String> insertPaths, final List<String> 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<T>(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<? extends T> all(Collection<? extends T> objects) {
return reactiveSupport.all(objects).collectList().block();
}
@Override
public TerminatingMutateInById<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> withCasProvided() {
return new ExecutableMutateInByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo,
durabilityLevel, expiry, removePaths, upsertPaths, insertPaths, replacePaths, true);
}
}
}

View File

@@ -22,4 +22,4 @@ package org.springframework.data.couchbase.core;
public interface FluentCouchbaseOperations extends ExecutableUpsertByIdOperation, ExecutableInsertByIdOperation,
ExecutableReplaceByIdOperation, ExecutableFindByIdOperation, ExecutableFindFromReplicasByIdOperation,
ExecutableFindByQueryOperation, ExecutableFindByAnalyticsOperation, ExecutableExistsByIdOperation,
ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation {}
ExecutableRemoveByIdOperation, ExecutableRemoveByQueryOperation, ExecutableMutateInByIdOperation {}

View File

@@ -44,6 +44,7 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @author Michael Reiche
* @author Jorge Rodriguez Martin
* @author Carlos Espinaco
* @author Tigran Babloyan
*/
public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware {
@@ -183,6 +184,11 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A
return new ReactiveUpsertByIdOperationSupport(this).upsertById(domainType);
}
@Override
public <T> ReactiveMutateInById<T> mutateInById(Class<T> domainType) {
return new ReactiveMutateInByIdOperationSupport(this).mutateInById(domainType);
}
@Override
public String getBucketName() {
return clientFactory.getBucket().name();

View File

@@ -22,4 +22,4 @@ package org.springframework.data.couchbase.core;
public interface ReactiveFluentCouchbaseOperations extends ReactiveUpsertByIdOperation, ReactiveInsertByIdOperation,
ReactiveReplaceByIdOperation, ReactiveFindByIdOperation, ReactiveExistsByIdOperation,
ReactiveFindByAnalyticsOperation, ReactiveFindFromReplicasByIdOperation, ReactiveFindByQueryOperation,
ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation {}
ReactiveRemoveByIdOperation, ReactiveRemoveByQueryOperation, ReactiveMutateInByIdOperation {}

View File

@@ -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.
*/
<T> ReactiveMutateInById<T> mutateInById(Class<T> domainType);
/**
* Terminating operations invoking the actual execution.
*/
interface TerminatingMutateInById<T> extends OneAndAllEntityReactive<T> {
/**
* Mutate one entity.
*
* @return Upserted entity.
*/
@Override
Mono<T> one(T object);
/**
* Mutate a collection of entities.
*
* @return Inserted entities
*/
@Override
Flux<? extends T> all(Collection<? extends T> objects);
}
interface MutateInByIdWithPaths<T> extends TerminatingMutateInById<T>, WithMutateInPaths<T> {
/**
* 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<T> 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<T> 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<T> 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<T> 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<T> withCasProvided();
}
/**
* Fluent method to specify options.
*
* @param <T> the entity type to use.
*/
interface MutateInByIdWithOptions<T> extends MutateInByIdWithPaths<T>, WithMutateInOptions<T> {
/**
* Fluent method to specify options to use for execution
*
* @param options to use for execution
*/
@Override
TerminatingMutateInById<T> withOptions(MutateInOptions options);
}
/**
* Fluent method to specify the collection.
*
* @param <T> the entity type to use for the results.
*/
interface MutateInByIdInCollection<T> extends MutateInByIdWithOptions<T>, InCollection<Object> {
/**
* With a different collection
*
* @param collection the collection to use.
*/
@Override
MutateInByIdWithOptions<T> inCollection(String collection);
}
/**
* Fluent method to specify the scope.
*
* @param <T> the entity type to use for the results.
*/
interface MutateInByIdInScope<T> extends MutateInByIdInCollection<T>, InScope<Object> {
/**
* With a different scope
*
* @param scope the scope to use.
*/
@Override
MutateInByIdInCollection<T> inScope(String scope);
}
interface MutateInByIdWithDurability<T> extends MutateInByIdInScope<T>, WithDurability<T> {
@Override
MutateInByIdInScope<T> withDurability(DurabilityLevel durabilityLevel);
@Override
MutateInByIdInScope<T> withDurability(PersistTo persistTo, ReplicateTo replicateTo);
}
interface MutateInByIdWithExpiry<T> extends MutateInByIdWithDurability<T>, WithExpiry<T> {
@Override
MutateInByIdWithDurability<T> withExpiry(Duration expiry);
}
/**
* Provides methods for constructing KV operations in a fluent way.
*
* @param <T> the entity type to upsert
*/
interface ReactiveMutateInById<T> extends MutateInByIdWithExpiry<T> {}
}

View File

@@ -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 <T> ReactiveMutateInById<T> mutateInById(final Class<T> 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<T> implements ReactiveMutateInById<T> {
private final ReactiveCouchbaseTemplate template;
private final Class<T> 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<String> removePaths = new ArrayList<>();
private final List<String> upsertPaths = new ArrayList<>();
private final List<String> insertPaths = new ArrayList<>();
private final List<String> replacePaths = new ArrayList<>();
ReactiveMutateInByIdSupport(final ReactiveCouchbaseTemplate template, final Class<T> 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<String> removePaths,
final List<String> upsertPaths, final List<String> insertPaths, final List<String> 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<T> one(T object) {
PseudoArgs<MutateInOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
if (LOG.isDebugEnabled()) {
LOG.debug("upsertById object={} {}", object, pArgs);
}
Mono<T> 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<? extends T> all(Collection<? extends T> objects) {
return Flux.fromIterable(objects).flatMap(this::one);
}
@Override
public TerminatingMutateInById<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<MutateInSpec> getMutations(CouchbaseDocument document) {
List<MutateInSpec> 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;
}
}
}

View File

@@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory;
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.json.JsonObject;
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 class OptionsBuilder {
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 @@ public class OptionsBuilder {
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);

View File

@@ -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 <T> - the entity class
*/
public interface WithMutateInOptions<T> {
/**
* Specify options
*
* @param options The mutate options to use.
*/
Object withOptions(MutateInOptions options);
}

View File

@@ -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 <T> - the entity class
*/
public interface WithMutateInPaths<T> {
Object withRemovePaths(final String... removePaths);
Object withInsertPaths(final String... insertPaths);
Object withReplacePaths(final String... replacePaths);
Object withUpsertPaths(final String... upsertPaths);
}

View File

@@ -27,20 +27,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
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 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
.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() {

View File

@@ -28,20 +28,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
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 @@ class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationT
}
}
@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");

View File

@@ -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<String> roles;
}