Skip to content

Added Sub-Document mutations support #1660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/main/asciidoc/template.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
----
====
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +64,7 @@
* @author Simon Baslé
* @author Michael Reiche
* @author Graham Pople
* @author Tigran Babloyan
*/
public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator {

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -95,6 +96,11 @@ public <T> ExecutableUpsertById<T> upsertById(final Class<T> domainType) {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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> {}

}
Loading