© 2014-2022 The original author(s).

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

Preface

This reference documentation describes the general usage of the Spring Data Couchbase library.

Migrating from Spring Data Couchbase 3.x to 4.x

This chapter is a quick reference of what major changes have been introduced in 4.x and gives a high-level overview of things to consider when migrating.

Please note that implicitly the minimum Couchbase Server version has been bumped up to 5.5 and later, and we recommend running at least 6.0.x.

Configuration

Since the main objective was to migrate from the Java SDK 2 to 3, configuration has changed to adapt to the new SDK and also in the long run to prepare it for scopes and collections (but it can still be used without collection support).

XML Configuration support has been dropped, so only java/annotation based configuration is supported.

Your configuration still has to extend the AbstractCouchbaseConfiguration, but since RBAC (role-based access control) is now mandatory, different properties need to be overridden in order to be configured: getConnectionString, getUserName, getPassword and getBucketName. If you want to use a non-default scope optionally you can override the getScopeName method. Note that if you want to use certificate based authentication or you need to customize the password authentication, the authenticator method can be overridden to perform this task.

The new SDK still has an environment that is used to configure it, so you can override the configureEnvironment method and supply custom configuration if needed.

For more information, see Installation & Configuration.

Spring Boot Version Compatibility

Spring Boot 2.3.x or higher depends on Spring Data Couchbase 4.x. Earlier versions of Couchbase are not available because SDK 2 and 3 cannot live on the same classpath.

Entities

How to deal with entities has not changed, although since the SDK now does not ship annotations anymore only Spring-Data related annotations are supported.

Specifically:

  • com.couchbase.client.java.repository.annotation.Id became import org.springframework.data.annotation.Id

  • com.couchbase.client.java.repository.annotation.Field became import org.springframework.data.couchbase.core.mapping.Field

The org.springframework.data.couchbase.core.mapping.Document annotation stayed the same.

For more information, see Modeling Entities.

Automatic Index Management

Automatic Index Management has been redesigned to allow more flexible indexing. New annotations have been introduced and old ones like @ViewIndexed, @N1qlSecondaryIndexed and @N1qlPrimaryIndexed were removed.

For more information, see Automatic Index Management.

Template and ReactiveTemplate

Since the Couchbase SDK 3 removes support for RxJava and instead adds support for Reactor, both the couchbaseTemplate as well as the reactiveCouchbaseTemplate can be directly accessed from the AbstractCouchbaseConfiguration.

The template has been completely overhauled so that it now uses a fluent API to configure instead of many method overloads. This has the advantage that in the future we are able to extend the functionality without having to introduce more and more overloads that make it complicated to navigate.

The following table describes the method names in 3.x and compares them to their 4.x equivalents:

Table 1. Template Method Comparison
SDC 3.x SDC 4.x

save

upsertById

insert

insertById

update

replaceById

findById

findById

findByView

(removed)

findBySpatialView

(removed)

findByN1QL

findByQuery

findByN1QLProjection

findByQuery

queryN1QL

(call SDK directly)

exists

existsById

remove

removeById

execute

(call SDK directly)

In addition, the following methods have been added which were not available in 3.x:

Table 2. Template Additions in 4.x
Name Description

removeByQuery

Allows to remove entities through a N1QL query

findByAnalytics

Performs a find through the analytics service

findFromReplicasById

Like findById, but takes replicas into account

We tried to unify and align the APIs more closely to the underlying SDK semantics so they are easier to correlate and navigate.

For more information, see Template & direct operations.

Repositories & Queries

  • org.springframework.data.couchbase.core.query.Query became org.springframework.data.couchbase.repository.Query

  • org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository has been removed. Consider extending ReactiveSortingRepository or ReactiveCouchbaseRepository

  • org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository has been removed. Consider extending PagingAndSortingRepository or CouchbaseRepository

Support for views has been removed and N1QL queries are now the first-class citizens for all custom repository methods as well as the built-in ones by default.

The behavior itself has not changed over the previous version on how the query derivation is supposed to work. Should you encounter any queries that worked in the past and now do not work anymore please let us know.

It is possible to override the default scan consistency for N1QL queries through the new ScanConsistency annotation.

The method getCouchbaseOperations() has also been removed. You can still access all methods from the native Java SDK via the class CouchbaseTemplate or Cluster:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.stereotype.Service;
import com.couchbase.client.java.Cluster;

@Service
public class MyService {

    @Autowired
    private CouchbaseTemplate couchbaseTemplate;

    @Autowired
    private Cluster cluster;
}

See Couchbase repositories for more information.

Full Text Search (FTS)

The FTS API has been simplified and now can be accessed via the Cluster class:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.search.result.SearchResult;
import com.couchbase.client.java.search.result.SearchRow;
import com.couchbase.client.core.error.CouchbaseException;

@Service
public class MyService {

    @Autowired
    private Cluster cluster;

    public void myMethod() {
        try {
          final SearchResult result = cluster
            .searchQuery("index", SearchQuery.queryString("query"));

          for (SearchRow row : result.rows()) {
            System.out.println("Found row: " + row);
          }

          System.out.println("Reported total rows: "
            + result.metaData().metrics().totalRows());
        } catch (CouchbaseException ex) {
          ex.printStackTrace();
        }
    }
}

See the FTS Documentation for more information.

Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/upgrade.adoc[leveloffset=+1]

Reference Documentation

1. Installation & Configuration

This chapter describes the common installation and configuration steps needed when working with the library.

1.1. Installation

All versions intended for production use are distributed across Maven Central and the Spring release repository. As a result, the library can be included like any other maven dependency:

Example 1. Including the dependency through maven
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-couchbase</artifactId>
    <version>5.1.0-SNAPSHOT</version>
</dependency>

This will pull in several dependencies, including the underlying Couchbase Java SDK, common Spring dependencies and also Jackson as the JSON mapping infrastructure.

You can also grab snapshots from the spring snapshot repository ( https://repo.spring.io/libs-snapshot ) and milestone releases from the spring milestone repository ( https://repo.spring.io/libs-milestone ). Here is an example on how to use the current SNAPSHOT dependency:

Example 2. Using a snapshot version
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-couchbase</artifactId>
  <version>${version}-SNAPSHOT</version>
</dependency>

<repository>
  <id>spring-libs-snapshot</id>
  <name>Spring Snapshot Repository</name>
  <url>https://repo.spring.io/libs-snapshot</url>
</repository>

Once you have all needed dependencies on the classpath, you can start configuring it. Only Java config is supported (XML config has been removed in 4.0).

1.2. Annotation-based Configuration ("JavaConfig")

To get started, all you need to do is subclcass the AbstractCouchbaseConfiguration and implement the abstract methods.

Example 3. Extending the AbstractCouchbaseConfiguration
@Configuration
public class Config extends AbstractCouchbaseConfiguration {

    @Override
    public String getConnectionString() {
        return "couchbase://127.0.0.1";
    }

    @Override
    public String getUserName() {
        return "Administrator";
    }

    @Override
    public String getPassword() {
        return "password";
    }

    @Override
    public String getBucketName() {
        return "travel-sample";
    }
}

The connection string is made up of a list of hosts and an optional scheme (couchbase://) as shown in the code above. All you need to provide is a list of Couchbase nodes to bootstrap into (separated by a ,). Please note that while one host is sufficient in development, it is recommended to add 3 to 5 bootstrap nodes here. Couchbase will pick up all nodes from the cluster automatically, but it could be the case that the only node you’ve provided is experiencing issues while you are starting the application.

The userName and password are configured in your Couchbase Server cluster through RBAC (role-based access control). The bucketName reflects the bucket you want to use for this configuration.

Additionally, the SDK environment can be tuned by overriding the configureEnvironment method which takes a ClusterEnvironment.Builder to return a configured ClusterEnvironment.

Many more things can be customized and overridden as custom beans from this configuration (for example repositories, validation and custom converters).

If you use SyncGateway and CouchbaseMobile, you may run into problem with fields prefixed by _. Since Spring Data Couchbase by default stores the type information as a _class attribute this can be problematic. Override typeKey() (for example to return MappingCouchbaseConverter.TYPEKEY_SYNCGATEWAY_COMPATIBLE) to change the name of said attribute.

If you start your application, you should see Couchbase INFO level logging in the logs, indicating that the underlying Couchbase Java SDK is connecting to the database. If any errors are reported, make sure that the given credentials and host information are correct.

2. Modeling Entities

This chapter describes how to model Entities and explains their counterpart representation in Couchbase Server itself.

Unresolved directive in entity.adoc - include::../../../../spring-data-commons/src/main/asciidoc/object-mapping.adoc[leveloffset=+1]

2.1. Documents and Fields

All entities should be annotated with the @Document annotation, but it is not a requirement.

Also, every field in the entity should be annotated with the @Field annotation. While this is - strictly speaking - optional, it helps to reduce edge cases and clearly shows the intent and design of the entity. It can also be used to store the field under a different name.

There is also a special @Id annotation which needs to be always in place. Best practice is to also name the property id.

Here is a very simple User entity:

Example 4. A simple Document with Fields
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Field;
import org.springframework.data.couchbase.core.mapping.Document;

@Document
public class User {

    @Id
    private String id;

    @Field
    private String firstname;

    @Field
    private String lastname;

    public User(String id, String firstname, String lastname) {
        this.id = id;
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public String getId() {
        return id;
    }

    public String getFirstname() {
        return firstname;
    }

    public String getLastname() {
        return lastname;
    }
}

Couchbase Server supports automatic expiration for documents. The library implements support for it through the @Document annotation. You can set a expiry value which translates to the number of seconds until the document gets removed automatically. If you want to make it expire in 10 seconds after mutation, set it like @Document(expiry = 10). Alternatively, you can configure the expiry using Spring’s property support and the expiryExpression parameter, to allow for dynamically changing the expiry value. For example: @Document(expiryExpression = "${valid.document.expiry}"). The property must be resolvable to an int value and the two approaches cannot be mixed.

If you want a different representation of the field name inside the document in contrast to the field name used in your entity, you can set a different name on the @Field annotation. For example if you want to keep your documents small you can set the firstname field to @Field("fname"). In the JSON document, you’ll see {"fname": ".."} instead of {"firstname": ".."}.

The @Id annotation needs to be present because every document in Couchbase needs a unique key. This key needs to be any string with a length of maximum 250 characters. Feel free to use whatever fits your use case, be it a UUID, an email address or anything else.

Writes to Couchbase-Server buckets can optionally be assigned durability requirements; which instruct Couchbase Server to update the specified document on multiple nodes in memory and/or disk locations across the cluster; before considering the write to be committed. Default durability requirements can also be configured through the @Document annotation. For example: @Document(durabilityLevel = DurabilityLevel.MAJORITY) will force mutations to be replicated to a majority of the Data Service nodes.

2.2. Datatypes and Converters

The storage format of choice is JSON. It is great, but like many data representations it allows less datatypes than you could express in Java directly. Therefore, for all non-primitive types some form of conversion to and from supported types needs to happen.

For the following entity field types, you don’t need to add special handling:

Table 3. Primitive Types
Java Type JSON Representation

string

string

boolean

boolean

byte

number

short

number

int

number

long

number

float

number

double

number

null

Ignored on write

Since JSON supports objects ("maps") and lists, Map and List types can be converted naturally. If they only contain primitive field types from the last paragraph, you don’t need to add special handling too. Here is an example:

Example 5. A Document with Map and List
@Document
public class User {

    @Id
    private String id;

    @Field
    private List<String> firstnames;

    @Field
    private Map<String, Integer> childrenAges;

    public User(String id, List<String> firstnames, Map<String, Integer> childrenAges) {
        this.id = id;
        this.firstnames = firstnames;
        this.childrenAges = childrenAges;
    }

}

Storing a user with some sample data could look like this as a JSON representation:

Example 6. A Document with Map and List - JSON
{
    "_class": "foo.User",
    "childrenAges": {
        "Alice": 10,
        "Bob": 5
    },
    "firstnames": [
        "Foo",
        "Bar",
        "Baz"
    ]
}

You don’t need to break everything down to primitive types and Lists/Maps all the time. Of course, you can also compose other objects out of those primitive values. Let’s modify the last example so that we want to store a List of Children:

Example 7. A Document with composed objects
@Document
public class User {

    @Id
    private String id;

    @Field
    private List<String> firstnames;

    @Field
    private List<Child> children;

    public User(String id, List<String> firstnames, List<Child> children) {
        this.id = id;
        this.firstnames = firstnames;
        this.children = children;
    }

    static class Child {
        private String name;
        private int age;

        Child(String name, int age) {
            this.name = name;
            this.age = age;
        }

    }

}

A populated object can look like:

Example 8. A Document with composed objects - JSON
{
  "_class": "foo.User",
  "children": [
    {
      "age": 4,
      "name": "Alice"
    },
    {
      "age": 3,
      "name": "Bob"
    }
  ],
  "firstnames": [
    "Foo",
    "Bar",
    "Baz"
  ]
}

Most of the time, you also need to store a temporal value like a Date. Since it can’t be stored directly in JSON, a conversion needs to happen. The library implements default converters for Date, Calendar and JodaTime types (if on the classpath). All of those are represented by default in the document as a unix timestamp (number). You can always override the default behavior with custom converters as shown later. Here is an example:

Example 9. A Document with Date and Calendar
@Document
public class BlogPost {

    @Id
    private String id;

    @Field
    private Date created;

    @Field
    private Calendar updated;

    @Field
    private String title;

    public BlogPost(String id, Date created, Calendar updated, String title) {
        this.id = id;
        this.created = created;
        this.updated = updated;
        this.title = title;
    }

}

A populated object can look like:

Example 10. A Document with Date and Calendar - JSON
{
  "title": "a blog post title",
  "_class": "foo.BlogPost",
  "updated": 1394610843,
  "created": 1394610843897
}

Optionally, Date can be converted to and from ISO-8601 compliant strings by setting system property org.springframework.data.couchbase.useISOStringConverterForDate to true. If you want to override a converter or implement your own one, this is also possible. The library implements the general Spring Converter pattern. You can plug in custom converters on bean creation time in your configuration. Here’s how you can configure it (in your overridden AbstractCouchbaseConfiguration):

Example 11. Custom Converters
@Override
public CustomConversions customConversions() {
    return new CustomConversions(Arrays.asList(FooToBarConverter.INSTANCE, BarToFooConverter.INSTANCE));
}

@WritingConverter
public static enum FooToBarConverter implements Converter<Foo, Bar> {
    INSTANCE;

    @Override
    public Bar convert(Foo source) {
        return /* do your conversion here */;
    }

}

@ReadingConverter
public static enum BarToFooConverter implements Converter<Bar, Foo> {
    INSTANCE;

    @Override
    public Foo convert(Bar source) {
        return /* do your conversion here */;
    }

}

There are a few things to keep in mind with custom conversions:

  • To make it unambiguous, always use the @WritingConverter and @ReadingConverter annotations on your converters. Especially if you are dealing with primitive type conversions, this will help to reduce possible wrong conversions.

  • If you implement a writing converter, make sure to decode into primitive types, maps and lists only. If you need more complex object types, use the CouchbaseDocument and CouchbaseList types, which are also understood by the underlying translation engine. Your best bet is to stick with as simple as possible conversions.

  • Always put more special converters before generic converters to avoid the case where the wrong converter gets executed.

  • For dates, reading converters should be able to read from any Number (not just Long). This is required for N1QL support.

2.3. Optimistic Locking

In certain situations you may want to ensure that you are not overwriting another users changes when you perform a mutation operation on a document. For this you have three choices: Transactions (since Couchbase 6.5), pessimistic concurrency (locking) or optimistic concurrency.

Optimistic concurrency tends to provide better performance than pessimistic concurrency or transactions, because no actual locks are held on the data and no extra information is stored about the operation (no transaction log).

To implement optimistic locking, Couchbase uses a CAS (compare and swap) approach. When a document is mutated, the CAS value also changes. The CAS is opaque to the client, the only thing you need to know is that it changes when the content or a meta information changes too.

In other datastores, similar behavior can be achieved through an arbitrary version field with a incrementing counter. Since Couchbase supports this in a much better fashion, it is easy to implement. If you want automatic optimistic locking support, all you need to do is add a @Version annotation on a long field like this:

Example 12. A Document with optimistic locking.
@Document
public class User {

        @Version
        private long version;

        // constructor, getters, setters...
}

If you load a document through the template or repository, the version field will be automatically populated with the current CAS value. It is important to note that you shouldn’t access the field or even change it on your own. Once you save the document back, it will either succeed or fail with a OptimisticLockingFailureException. If you get such an exception, the further approach depends on what you want to achieve application wise. You should either retry the complete load-update-write cycle or propagate the error to the upper layers for proper handling.

2.4. Validation

The library supports JSR 303 validation, which is based on annotations directly in your entities. Of course you can add all kinds of validation in your service layer, but this way its nicely coupled to your actual entities.

To make it work, you need to include two additional dependencies. JSR 303 and a library that implements it, like the one supported by hibernate:

Example 13. Validation dependencies
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependency>

Now you need to add two beans to your configuration:

Example 14. Validation beans
@Bean
public LocalValidatorFactoryBean validator() {
    return new LocalValidatorFactoryBean();
}

@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
    return new ValidatingCouchbaseEventListener(validator());
}

Now you can annotate your fields with JSR303 annotations. If a validation on save() fails, a ConstraintViolationException is thrown.

Example 15. Sample Validation Annotation
@Size(min = 10)
@Field
private String name;

2.5. Auditing

Entities can be automatically audited (tracing which user created the object, updated the object, and at what times) through Spring Data auditing mechanisms.

First, note that only entities that have a @Version annotated field can be audited for creation (otherwise the framework will interpret a creation as an update).

Auditing works by annotating fields with @CreatedBy, @CreatedDate, @LastModifiedBy and @LastModifiedDate. The framework will automatically inject the correct values on those fields when persisting the entity. The xxxDate annotations must be put on a Date field (or compatible, eg. jodatime classes) while the xxxBy annotations can be put on fields of any class T (albeit both fields must be of the same type).

To configure auditing, first you need to have an auditor aware bean in the context. Said bean must be of type AuditorAware<T> (allowing to produce a value that can be stored in the xxxBy fields of type T we saw earlier). Secondly, you must activate auditing in your @Configuration class by using the @EnableCouchbaseAuditing annotation.

Here is an example:

Example 16. Sample Auditing Entity
@Document
public class AuditedItem {

  @Id
  private final String id;

  private String value;

  @CreatedBy
  private String creator;

  @LastModifiedBy
  private String lastModifiedBy;

  @LastModifiedDate
  private Date lastModification;

  @CreatedDate
  private Date creationDate;

  @Version
  private long version;

  //..omitted constructor/getters/setters/...
}

Notice both @CreatedBy and @LastModifiedBy are both put on a String field, so our AuditorAware must work with String.

Example 17. Sample AuditorAware implementation
public class NaiveAuditorAware implements AuditorAware<String> {

  private String auditor = "auditor";

  @Override
  public String getCurrentAuditor() {
    return auditor;
  }

  public void setAuditor(String auditor) {
    this.auditor = auditor;
  }
}

To tie all that together, we use the java configuration both to declare an AuditorAware bean and to activate auditing:

Example 18. Sample Auditing Configuration
@Configuration
@EnableCouchbaseAuditing //this activates auditing
public class AuditConfiguration extends AbstractCouchbaseConfiguration {

    //... a few abstract methods omitted here

    // this creates the auditor aware bean that will feed the annotations
    @Bean
    public NaiveAuditorAware testAuditorAware() {
      return new NaiveAuditorAware();
    }

3. Auto generating keys

This chapter describes how couchbase document keys can be auto-generated using builtin mechanisms. There are two types of auto-generation strategies supported.

The maximum key length supported by couchbase is 250 bytes.

3.1. Configuration

Keys to be auto-generated should be annotated with @GeneratedValue. The default strategy is USE_ATTRIBUTES. Prefix and suffix for the key can be provided as part of the entity itself, these values are not persisted, they are only used for key generation. The prefixes and suffixes are ordered using the order value. The default order is 0, multiple prefixes without order will overwrite the previous. If a value for id is already available, auto-generation will be skipped. The delimiter for concatenation can be provided using delimiter, the default delimiter is ..

Example 19. Annotation for GeneratedValue
@Document
public class User {
     @Id @GeneratedValue(strategy = USE_ATTRIBUTES, delimiter = ".")
     private String id;
     @IdPrefix(order=0)
     private String userPrefix;
     @IdSuffix(order=0)
     private String userSuffix;
     ...
}

3.2. Key generation using attributes

It is a common practice to generate keys using a combination of the document attributes. Key generation using attributes concatenates all the attribute values annotated with IdAttribute, based on the ordering provided similar to prefixes and suffixes.

Example 20. Annotation for IdAttribute
@Document
public class User {
     @Id @GeneratedValue(strategy = USE_ATTRIBUTES)
     private String id;
     @IdAttribute
     private String userid;
     ...
}

3.3. Key generation using uuid

This auto-generation uses UUID random generator to generate document keys consuming 16 bytes of key space. This mechanism is only recommended for test scaffolding.

Example 21. Annotation for Unique key generation
@Document
public class User {
     @Id @GeneratedValue(strategy = UNIQUE)
     private String id;
     ...
}

Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/repositories.adoc[]

4. Couchbase repositories

The goal of Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores.

By default, operations are backed by Key/Value if they are single-document operations and the ID is known. For all other operations by default N1QL queries are generated, and as a result proper indexes must be created for performant data access.

Note that you can tune the consistency you want for your queries (see Querying with consistency) and have different repositories backed by different buckets (see [couchbase.repository.multibucket])

4.1. Configuration

While support for repositories is always present, you need to enable them in general or for a specific namespace. If you extend AbstractCouchbaseConfiguration, just use the @EnableCouchbaseRepositories annotation. It provides lots of possible options to narrow or customize the search path, one of the most common ones is basePackages.

Also note that if you are running inside spring boot, the autoconfig support already sets up the annotation for you so you only need to use it if you want to override the defaults.

Example 22. Annotation-Based Repository Setup
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.couchbase.example.repos"})
public class Config extends AbstractCouchbaseConfiguration {
    //...
}

An advanced usage is described in [couchbase.repository.multibucket].

4.2. Usage

In the simplest case, your repository will extend the CrudRepository<T, String>, where T is the entity that you want to expose. Let’s look at a repository for a UserInfo:

Example 23. A UserInfo repository
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserInfo, String> {
}

Please note that this is just an interface and not an actual class. In the background, when your context gets initialized, actual implementations for your repository descriptions get created and you can access them through regular beans. This means you will save lots of boilerplate code while still exposing full CRUD semantics to your service layer and application.

Now, let’s imagine we @Autowire the UserRepository to a class that makes use of it. What methods do we have available?

Table 4. Exposed methods on the UserRepository
Method Description

UserInfo save(UserInfo entity)

Save the given entity.

Iterable<UserInfo> save(Iterable<UserInfo> entity)

Save the list of entities.

UserInfo findOne(String id)

Find a entity by its unique id.

boolean exists(String id)

Check if a given entity exists by its unique id.

Iterable<UserInfo> findAll()

Find all entities by this type in the bucket.

Iterable<UserInfo> findAll(Iterable<String> ids)

Find all entities by this type and the given list of ids.

long count()

Count the number of entities in the bucket.

void delete(String id)

Delete the entity by its id.

void delete(UserInfo entity)

Delete the entity.

void delete(Iterable<UserInfo> entities)

Delete all given entities.

void deleteAll()

Delete all entities by type in the bucket.

Now that’s awesome! Just by defining an interface we get full CRUD functionality on top of our managed entity.

While the exposed methods provide you with a great variety of access patterns, very often you need to define custom ones. You can do this by adding method declarations to your interface, which will be automatically resolved to requests in the background, as we’ll see in the next sections.

4.3. Repositories and Querying

4.3.1. N1QL based querying

Prerequisite is to have created a PRIMARY INDEX on the bucket where the entities will be stored.

Here is an example:

Example 24. An extended UserInfo repository with N1QL queries
public interface UserRepository extends CrudRepository<UserInfo, String> {

    @Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}")
    List<UserInfo> findAllAdmins();

    List<UserInfo> findByFirstname(String fname);
}

Here we see two N1QL-backed ways of querying.

The first method uses the Query annotation to provide a N1QL statement inline. SpEL (Spring Expression Language) is supported by surrounding SpEL expression blocks between #{ and }. A few N1QL-specific values are provided through SpEL:

  • #n1ql.selectEntity allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value).

  • #n1ql.filter in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information.

  • #n1ql.bucket will be replaced by the name of the bucket the entity is stored in, escaped in backticks.

  • #n1ql.scope will be replaced by the name of the scope the entity is stored in, escaped in backticks.

  • #n1ql.collection will be replaced by the name of the collection the entity is stored in, escaped in backticks.

  • #n1ql.fields will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity.

  • #n1ql.delete will be replaced by the delete from statement.

  • #n1ql.returning will be replaced by returning clause needed for reconstructing entity.

We recommend that you always use the selectEntity SpEL and a WHERE clause with a filter SpEL (since otherwise your query could be impacted by entities from other repositories).

String-based queries support parametrized queries. You can either use positional placeholders like “$1”, in which case each of the method parameters will map, in order, to $1, $2, $3…​ Alternatively, you can use named placeholders using the “$someString” syntax. Method parameters will be matched with their corresponding placeholder using the parameter’s name, which can be overridden by annotating each parameter (except a Pageable or Sort) with @Param (eg. @Param("someString")). You cannot mix the two approaches in your query and will get an IllegalArgumentException if you do.

Note that you can mix N1QL placeholders and SpEL. N1QL placeholders will still consider all method parameters, so be sure to use the correct index like in the example below:

Example 25. An inline query that mixes SpEL and N1QL placeholders
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND #{[0]} = $2")
public List<User> findUsersByDynamicCriteria(String criteriaField, Object criteriaValue)

This allows you to generate queries that would work similarly to eg. AND name = "someName" or AND age = 3, with a single method declaration.

You can also do single projections in your N1QL queries (provided it selects only one field and returns only one result, usually an aggregation like COUNT, AVG, MAX…​). Such projection would have a simple return type like long, boolean or String. This is NOT intended for projections to DTOs.

Another example:
#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND test = $1
is equivalent to
SELECT #{#n1ql.fields} FROM #{#n1ql.collection} WHERE #{#n1ql.filter} AND test = $1

A practical application of SpEL with Spring Security

SpEL can be useful when you want to do a query depending on data injected by other Spring components, like Spring Security. Here is what you need to do to extend the SpEL context to get access to such external data.

First, you need to implement an EvaluationContextExtension (use the support class as below):

class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {

  @Override
  public String getExtensionId() {
    return "security";
  }

  @Override
  public SecurityExpressionRoot getRootObject() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return new SecurityExpressionRoot(authentication) {};
  }
}

Then all you need to do for Spring Data Couchbase to be able to access associated SpEL values is to declare a corresponding bean in your configuration:

@Bean
EvaluationContextExtension securityExtension() {
    return new SecurityEvaluationContextExtension();
}

This could be useful to craft a query according to the role of the connected user for instance:

@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND " +
"role = '?#{hasRole('ROLE_ADMIN') ? 'public_admin' : 'admin'}'")
List<UserInfo> findAllAdmins(); //only ROLE_ADMIN users will see hidden admins

Delete query example:

@Query("#{#n1ql.delete} WHERE #{#n1ql.filter} AND " +
"username = $1 #{#n1ql.returning}")
UserInfo removeUser(String username);

The second method uses Spring-Data’s query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: SELECT …​ FROM …​ WHERE firstName = "valueOfFnameAtRuntime". You can combine these criteria, even do a count with a name like countByFirstname or a limit with a name like findFirst3ByLastname…​

Actually the generated N1QL query will also contain an additional N1QL criteria in order to only select documents that match the repository’s entity class.

Most Spring-Data keywords are supported: .Supported keywords inside @Query (N1QL) method names

Keyword Sample N1QL WHERE clause snippet

And

findByLastnameAndFirstname

lastName = a AND firstName = b

Or

findByLastnameOrFirstname

lastName = a OR firstName = b

Is,Equals

findByField,findByFieldEquals

field = a

IsNot,Not

findByFieldIsNot

field != a

Between

findByFieldBetween

field BETWEEN a AND b

IsLessThan,LessThan,IsBefore,Before

findByFieldIsLessThan,findByFieldBefore

field < a

IsLessThanEqual,LessThanEqual

findByFieldIsLessThanEqual

field ⇐ a

IsGreaterThan,GreaterThan,IsAfter,After

findByFieldIsGreaterThan,findByFieldAfter

field > a

IsGreaterThanEqual,GreaterThanEqual

findByFieldGreaterThanEqual

field >= a

IsNull

findByFieldIsNull

field IS NULL

IsNotNull,NotNull

findByFieldIsNotNull

field IS NOT NULL

IsLike,Like

findByFieldLike

field LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)

IsNotLike,NotLike

findByFieldNotLike

field NOT LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)

IsStartingWith,StartingWith,StartsWith

findByFieldStartingWith

field LIKE "a%" - a should be a String prefix

IsEndingWith,EndingWith,EndsWith

findByFieldEndingWith

field LIKE "%a" - a should be a String suffix

IsContaining,Containing,Contains

findByFieldContains

field LIKE "%a%" - a should be a String

IsNotContaining,NotContaining,NotContains

findByFieldNotContaining

field NOT LIKE "%a%" - a should be a String

IsIn,In

findByFieldIn

field IN array - note that the next parameter value (or its children if a collection/array) should be compatible for storage in a JsonArray)

IsNotIn,NotIn

findByFieldNotIn

field NOT IN array - note that the next parameter value (or its children if a collection/array) should be compatible for storage in a JsonArray)

IsTrue,True

findByFieldIsTrue

field = TRUE

IsFalse,False

findByFieldFalse

field = FALSE

MatchesRegex,Matches,Regex

findByFieldMatches

REGEXP_LIKE(field, "a") - note that the ignoreCase is ignored here, a is a regular expression in String form

Exists

findByFieldExists

field IS NOT MISSING - used to verify that the JSON contains this attribute

OrderBy

findByFieldOrderByLastnameDesc

field = a ORDER BY lastname DESC

IgnoreCase

findByFieldIgnoreCase

LOWER(field) = LOWER("a") - a must be a String

You can use both counting queries and [repositories.limit-query-result] features with this approach.

With N1QL, another possible interface for the repository is the PagingAndSortingRepository one (which extends CrudRepository). It adds two methods:

Table 5. Exposed methods on the PagingAndSortingRepository
Method Description

Iterable<T> findAll(Sort sort);

Allows to retrieve all relevant entities while sorting on one of their attributes.

Page<T> findAll(Pageable pageable);

Allows to retrieve your entities in pages. The returned Page allows to easily get the next page’s Pageable as well as the list of items. For the first call, use new PageRequest(0, pageSize) as Pageable.

You can also use Page and Slice as method return types as well with a N1QL backed repository.
If pageable and sort parameters are used with inline queries, there should not be any order by, limit or offset clause in the inline query itself otherwise the server would reject the query as malformed.

4.3.2. Automatic Index Management

By default, it is expected that the user creates and manages optimal indexes for their queries. Especially in the early stages of development, it can come in handy to automatically create indexes to get going quickly.

For N1QL, the following annotations are provided which need to be attached to the entity (either on the class or the field):

  • @QueryIndexed: Placed on a field to signal that this field should be part of the index

  • @CompositeQueryIndex: Placed on the class to signal that an index on more than one field (composite) should be created.

  • @CompositeQueryIndexes: If more than one CompositeQueryIndex should be created, this annotation will take a list of them.

For example, this is how you define a composite index on an entity:

Example 26. Composite index on two fields with ordering
@Document
@CompositeQueryIndex(fields = {"id", "name desc"})
public class Airline {
   @Id
   String id;

	@QueryIndexed
	String name;

	@PersistenceConstructor
	public Airline(String id, String name) {
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}

}

By default, index creation is disabled. If you want to enable it you need to override it on the configuration:

Example 27. Enable auto index creation
@Override
protected boolean autoIndexCreation() {
 return true;
}

4.3.3. Querying with consistency

By default repository queries that use N1QL use the NOT_BOUNDED scan consistency. This means that results return quickly, but the data from the index may not yet contain data from previously written operations (called eventual consistency). If you need "ready your own write" semantics for a query, you need to use the @ScanConsistency annotation. Here is an example:

Example 28. Using a different scan consistency
@Repository
public interface AirportRepository extends PagingAndSortingRepository<Airport, String> {

	@Override
	@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
	Iterable<Airport> findAll();

}

4.3.4. DTO Projections

Spring Data Repositories usually return the domain model when using query methods. However, sometimes, you may need to alter the view of that model for various reasons. In this section, you will learn how to define projections to serve up simplified and reduced views of resources.

Look at the following domain model:

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

@Entity
public class Address {

  @Id @GeneratedValue
  private Long id;
  private String street, state, country;

  …
}

This Person has several attributes:

  • id is the primary key

  • firstName and lastName are data attributes

  • address is a link to another domain object

Now assume we create a corresponding repository as follows:

interface PersonRepository extends CrudRepository<Person, Long> {

  Person findPersonByFirstName(String firstName);
}

Spring Data will return the domain object including all of its attributes. There are two options just to retrieve the address attribute. One option is to define a repository for Address objects like this:

interface AddressRepository extends CrudRepository<Address, Long> {}

In this situation, using PersonRepository will still return the whole Person object. Using AddressRepository will return just the Address.

However, what if you do not want to expose address details at all? You can offer the consumer of your repository service an alternative by defining one or more projections.

Example 29. Simple Projection
interface NoAddresses {  (1)

  String getFirstName(); (2)

  String getLastName();  (3)
}

This projection has the following details:

1 A plain Java interface making it declarative.
2 Export the firstName.
3 Export the lastName.

The NoAddresses projection only has getters for firstName and lastName meaning that it will not serve up any address information. The query method definition returns in this case NoAdresses instead of Person.

interface PersonRepository extends CrudRepository<Person, Long> {

  NoAddresses findByFirstName(String firstName);
}

Projections declare a contract between the underlying type and the method signatures related to the exposed properties. Hence it is required to name getter methods according to the property name of the underlying type. If the underlying property is named firstName, then the getter method must be named getFirstName otherwise Spring Data is not able to look up the source property.

5. Reactive Couchbase repository

5.1. Introduction

This chapter describes the reactive repository support for couchbase. This builds on the core repository support explained in Couchbase repositories. So make sure you’ve got a sound understanding of the basic concepts explained there.

5.2. Reactive Composition Libraries

The Couchbase Java SDK 3.x moved from RxJava to Reactor, so it blends in very nicely with the reactive spring ecosystem.

Reactive Couchbase repositories provide project Reactor wrapper types and can be used by simply extending from one of the library-specific repository interfaces:

  • ReactiveCrudRepository

  • ReactiveSortingRepository

5.3. Usage

Let’s create a simple entity to start with:

Example 30. Sample Person entity
public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}

A corresponding repository implementation may look like this:

Example 31. Basic repository interface to persist Person entities
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {

  Flux<Person> findByFirstname(String firstname);

  Flux<Person> findByFirstname(Publisher<String> firstname);

  Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable);

  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);
}

For JavaConfig use the @EnableReactiveCouchbaseRepositories annotation. The annotation carries the very same attributes like the namespace element. If no base package is configured the infrastructure will scan the package of the annotated configuration class.

Also note that if you are using it in a spring boot setup you likely can omit the annotation since it is autoconfigured for you.

Example 32. JavaConfig for repositories
@Configuration
@EnableReactiveCouchbaseRepositories
class ApplicationConfig extends AbstractCouchbaseConfiguration {
	// ... (see configuration for details)
}

As our domain repository extends ReactiveSortingRepository it provides you with CRUD operations as well as methods for sorted access to the entities. Working with the repository instance is just a matter of dependency injecting it into a client.

Example 33. Sorted access to Person entities
public class PersonRepositoryTests {

    @Autowired
    ReactivePersonRepository repository;

    @Test
    public void sortsElementsCorrectly() {
      Flux<Person> persons = repository.findAll(Sort.by(new Order(ASC, "lastname")));
      assertNotNull(perons);
    }
}

5.4. Repositories and Querying

Spring Data’s Reactive Couchbase comes with full querying support already provided by the blocking Repositories and Querying

6. Template & direct operations

The template provides lower level access to the underlying database and also serves as the foundation for repositories. Any time a repository is too high-level for you needs chances are good that the templates will serve you well. Note that you can always drop into the SDK directly through the beans exposed on the AbstractCouchbaseConfiguration.

6.1. Supported operations

The template can be accessed through the couchbaseTemplate and reactiveCouchbaseTemplate beans out of your context. Once you’ve got a reference to it, you can run all kinds of operations against it. Other than through a repository, in a template you need to always specify the target entity type which you want to get converted.

The templates use a fluent-style API which allows you to chain in optional operators as needed. As an example, here is how you store a user and then find it again by its ID:

Example 34. Fluent template access
// Create an Entity
User user = new User(UUID.randomUUID().toString(), "firstname", "lastname");

// Upsert it
couchbaseTemplate.upsertById(User.class).one(user);

// Retrieve it again
User found = couchbaseTemplate.findById(User.class).one(user.getId());

If you wanted to use a custom (by default durability options from the @Document annotation will be used) durability requirement for the upsert operation you can chain it in:

Example 35. Upsert with durability
User modified = couchbaseTemplate
  .upsertById(User.class)
  .withDurability(DurabilityLevel.MAJORITY)
  .one(user);

In a similar fashion, you can perform a N1QL operation:

Example 36. N1QL query on the template
final List<User> foundUsers = couchbaseTemplate
  .findByQuery(User.class)
  .consistentWith(QueryScanConsistency.REQUEST_PLUS)
  .all();

6.2. Sub-Document Operations

Couchbase supports 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.

Example 37. MutateIn upsert on the template
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);

6.2.1. 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.

Example 38. MutateIn Multiple Operations
couchbaseTemplate.mutateInById(User.class)
    .withInsertPaths("roles", "subuser.firstname")
    .withRemovePaths("address.city")
    .withUpsertPaths("firstname")
    .withReplacePaths("address.street")
    .one(user);

6.2.2. 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:

Example 39. MutateIn With CAS
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);

7. Couchbase Transactions

Couchbase supports Distributed Transactions. This section documents how to use it with Spring Data Couchbase.

7.1. Requirements

  • Couchbase Server 6.6.1 or aabove.

  • Spring Data Couchbase 5.0.0-M5 or above.

  • NTP should be configured so nodes of the Couchbase cluster are in sync with time. The time being out of sync will not cause incorrect behavior, but can impact metadata cleanup.

  • Set spring.main.allow-bean-definition-overriding=true either in application.properties or as a SpringApplicationBuilder property.

  • The entity class must have an @Version Long property to hold the CAS value of the document.

7.2. Overview

The Spring Data Couchbase template operations insert, find, replace and delete and repository methods that use those calls can participate in a Couchbase Transaction. They can be executed in a transaction by using the @Transactional annotation, the CouchbaseTransactionalOperator, or in the lambda of a Couchbase Transaction.

7.3. Getting Started & Configuration

Couchbase Transactions are normally leveraged with a method annotated with @Transactional. The @Transactional operator is implemented with the CouchbaseTransactionManager which is supplied as a bean in the AbstractCouchbaseConfiguration. Couchbase Transactions can be used without defining a service class by using CouchbaseTransactionOperator which is also supplied as a bean in AbtractCouchbaseConfiguration. Couchbase Transactions can also be used directly using Spring Data Couchbase operations within a lambda Using Transactions

7.4. Transactions with @Transactional

@Transactional defines as transactional a method or all methods on a class.

When this annotation is declared at the class level, it applies as a default to all methods of the declaring class and its subclasses.

7.4.1. Attribute Semantics

In this release, the Couchbase Transactions ignores the rollback attributes. The transaction isolation level is read-committed;

Example 40. Transaction Configuration and Use by @Transactional
The Configuration
@Configuration
@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
@EnableTransactionManagement (1)
static class Config extends AbstractCouchbaseConfiguration {

  // Usual Setup
  @Override public String getConnectionString() { /* ... */ }
  @Override public String getUserName() { /* ... */ }
  @Override public String getPassword() { /* ... */ }
  @Override public String getBucketName() { /* ... */ }

  // Customization of transaction behavior is via the configureEnvironment() method
  @Override protected void configureEnvironment(final Builder builder) {
    builder.transactionsConfig(
      TransactionsConfig.builder().timeout(Duration.ofSeconds(30)));
  }
}
The Transactional Service Class

Note that the body of @Transactional methods can be re-executed if the transaction fails. It is imperative that everthing in the method body be idempotent.

import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

final CouchbaseOperations personOperations;
final ReactiveCouchbaseOperations reactivePersonOperations;

@Service (2)
public class PersonService {

  final CouchbaseOperations operations;
  final ReactiveCouchbaseOperations reactiveOperations;

  public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations reactiveOps) {
    operations = ops;
    reactiveOperations = reactiveOps;
  }

  // no annotation results in this method being executed not in a transaction
  public Person save(Person p) {
    return operations.save(p);
  }

  @Transactional
  public Person changeFirstName(String id, String newFirstName) {
    Person p = operations.findById(Person.class).one(id); (3)
    return operations.replaceById(Person.class).one(p.withFirstName(newFirstName);
  }

  @Transactional
  public Mono<Person> reactiveChangeFirstName(String id, String newFirstName) {
    return personOperationsRx.findById(Person.class).one(person.id())
        .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(newFirstName)));
  }

}
Using the @Transactional Service.
@Autowired PersonService personService; (4)

Person walterWhite = new Person( "Walter", "White");
Person p = personService.save(walterWhite); // this is not a transactional method
...
Person renamedPerson = personService.changeFirstName(walterWhite.getId(), "Ricky"); (5)

Functioning of the @Transactional method annotation requires

  1. the configuration class to be annotated with @EnableTransactionManagement;

  2. the service object with the annotated methods must be annotated with @Service;

  3. the body of the method is executed in a transaction.

  4. the service object with the annotated methods must be obtained via @Autowired.

  5. the call to the method must be made from a different class than service because calling an annotated method from the same class will not invoke the Method Interceptor that does the transaction processing.

7.5. Transactions with CouchbaseTransactionalOperator

CouchbaseTransactionalOperator can be used to construct a transaction in-line without creating a service class that uses @Transactional. CouchbaseTransactionalOperator is available as a bean and can be instantiated with @Autowired. If creating one explicitly, it must be created with CouchbaseTransactionalOperator.create(manager) (NOT TransactionalOperator.create(manager)).

Example 41. Transaction Access Using TransactionalOperator.execute()
@Autowired TransactionalOperator txOperator;
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;

Flux<Person> result = txOperator.execute((ctx) ->
  reactiveCouchbaseTemplate.findById(Person.class).one(person.id())
    .flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
 );

7.6. Transactions Directly with the SDK

Spring Data Couchbase works seamlessly with the Couchbase Java SDK for transaction processing. Spring Data Couchbase operations that can be executed in a transaction will work directly within the lambda of a transactions().run() without involving any of the Spring Transactions mechanisms. This is the most straight-forward way to leverage Couchbase Transactions in Spring Data Couchbase.

Please see the Reference Documentation

Example 42. Transaction Access - Blocking
@Autowired CouchbaseTemplate couchbaseTemplate;

TransactionResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> {
  Person p = couchbaseTemplate.findById(Person.class).one(personId);
  couchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt"));
});
Example 43. Transaction Access - Reactive
@Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;

Mono<TransactionResult> result = reactiveCouchbaseTemplate.getCouchbaseClientFactory().getCluster().reactive().transactions()
  .run(ctx ->
    reactiveCouchbaseTemplate.findById(Person.class).one(personId)
      .flatMap(p -> reactiveCouchbaseTemplate.replaceById(Person.class).one(p.withFirstName("Walt")))
  );

8. Collection Support

Couchbase supports Scopes and Collections. This section documents on how to use it with Spring Data Couchbase.

The try-cb-spring sample application is a working example of using Scopes and Collections in Spring Data Couchbase.

The 2021 Couchbase Connect presentation on Collections in Spring Data can be found at Presentation Only and Presentation with Slide Deck

8.1. Requirements

  • Couchbase Server 7.0 or above.

  • Spring Data Couchbase 4.3.1 or above.

8.2. Getting Started & Configuration

8.2.1. Scope and Collection Specification

There are several mechanisms of specifying scopes and collections, and these may be combined, or one mechanism may override another. First some definitions for scopes and collections. An unspecified scope indicates that the default scope is to be used, likewise, an unspecified collection indicates that the default collection is to be used. There are only three combinations of scopes and collections that are valid. (1) the default scope and the default collection; (2) the default scope and a non-default collection; and (3) a non-default scope and a non-default collection. It is not possible to have a non-default scope and a default collection as non-default scopes do not contain a default collections, neither can one be created.

A scope can be specified in the configuration:

@Configuration
static class Config extends AbstractCouchbaseConfiguration {

    // Usual Setup
    @Override public String getConnectionString() { /* ... */ }

    // optionally specify the scope in the Configuration
    @Override
    protected String getScopeName() {
        return "myScope"; // or a variable etc.;
    }

}

Scopes and Collections can be specified as annotations on entity classes and repositories:

@Document
@Scope("travel")
@Collection("airport")
public class Airport {...
@Scope("travel")
@Collection("airport")
public interface AirportRepository extends CouchbaseRepository<Airport, String> ...

Scopes and Collections can be specified on templates using the inScope(scopeName) and inCollection(collectionName) fluent APIs:

List<Airport> airports = template.findByQuery(Airport.class).inScope("archived").all()

Scopes and Collections can be specified on repositories that extend DynamicProxyable using the withScope(scopeName) and withCollection(collectionName) APIs:

public interface AirportRepository extends CouchbaseRepository<Airport, String>, DynamicProxyable<AirportRepository>{...}
...
List<Airport> airports = airportRepository.withScope("archived").findByName(iata);
The order of precedence is:
  1. inScope()/inCollection() of the template fluent api

  2. withScope()/withCollection() of the template/repository object

  3. annotation of the repository method

  4. annotation of the repository interface

  5. annotation of the entity object

  6. getScope() of the configuration

9. Couchbase Field Level Encrytpion

Couchbase supports Field Level Encryption. This section documents how to use it with Spring Data Couchbase.

9.1. Requirements

  • Spring Data Couchbase 5.0.0-RC1 or above.

9.2. Overview

Fields annotated with com.couchbase.client.java.encryption.annotation.Encrypted (@Encrypted) will be automatically encrypted on write and decrypted on read. Unencrypted fields can be migrated to encrypted by specifying @Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED).

9.3. Getting Started & Configuration

9.3.1. Dependencies

Field Level Encryption is available with the dependency ( see Field Level Encryption )

    <groupId>com.couchbase.client</groupId>
    <artifactId>couchbase-encryption</artifactId>

HashiCorp Vault Transit integration requires Spring Vault

    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>

9.3.2. Providing a CryptoManager

A CryptoManager needs to be provided by overriding the cryptoManager() method in AbstractCouchbaseConfiguration. This CryptoManager will be used by Spring Data Couchbase and also by Couchbase Java SDK direct calls made from a CouchbaseClientFactory.

@Override
protected CryptoManager cryptoManager() {
  KeyStore javaKeyStore = KeyStore.getInstance("MyKeyStoreType");
  FileInputStream fis = new java.io.FileInputStream("keyStoreName");
  char[] password = { 'a', 'b', 'c' };
  javaKeyStore.load(fis, password);
  Keyring keyring = new KeyStoreKeyring(javaKeyStore, keyName -> "swordfish");

  // AES-256 authenticated with HMAC SHA-512. Requires a 64-byte key.
  AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder().keyring(keyring).build();

  CryptoManager cryptoManager = DefaultCryptoManager.builder().decrypter(provider.decrypter())
    .defaultEncrypter(provider.encrypterForKey("myKey")).build();
}

9.3.3. Defining a Field as Encrypted.

  1. @Encrypted defines a field as encrypted.

  2. @Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED) defines a field that may or may not be encrypted when read. It will be encrypted when written.

  3. @Encrypted(encrypter = "<encrypterAlias>") specifies the alias of the encrypter to use for encryption. Note this is not the algorithm, but the name specified when adding the encrypter to the CryptoManager.

9.3.4. Example

Example 44. AbstractCouchbaseConfiguration
@Configuration
@EnableCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
@EnableReactiveCouchbaseRepositories("<parent-dir-of-repository-interfaces>")
static class Config extends AbstractCouchbaseConfiguration {

  // Usual Setup
  @Override public String getConnectionString() { /* ... */ }
  @Override public String getUserName() { /* ... */ }
  @Override public String getPassword() { /* ... */ }
  @Override public String getBucketName() { /* ... */ }

  /* provide a cryptoManager */
  @Override
  protected CryptoManager cryptoManager() {
    KeyStore javaKeyStore = KeyStore.getInstance("MyKeyStoreType");
    FileInputStream fis = new java.io.FileInputStream("keyStoreName");
    char[] password = { 'a', 'b', 'c' };
    javaKeyStore.load(fis, password);
    Keyring keyring = new KeyStoreKeyring(javaKeyStore, keyName -> "swordfish");

    // AES-256 authenticated with HMAC SHA-512. Requires a 64-byte key.
    AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder().keyring(keyring).build();

    CryptoManager cryptoManager = DefaultCryptoManager.builder().decrypter(provider.decrypter())
      .defaultEncrypter(provider.encrypterForKey("myKey")).build();
  }

}
Example 45. The Annotation in the Document
@Document
public class AddressWithEncStreet extends Address {

    private @Encrypted String encStreet;
    .
    .
Example 46. Usage in Code
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setCity("Santa Clara");
address.setEncStreet("Olcott Street");
addressEncryptedRepository.save(address);
Example 47. Resulting Document
{
  "_class": "AddressWithEncStreet",
   "city": "Santa Clara",
   "encrypted$encStreet": {
     "alg": "AEAD_AES_256_CBC_HMAC_SHA512",
     "ciphertext": "A/tJALmtixTxqj77ZUcUgMklIt3372DKD7l5FvbCzHNJMplbgQEv0RgSbxIfiRNr+uW2H7cokkcCW/F5YnQoXA==",
     "kid": "myKey"
   }
}

10. ANSI Joins

This chapter describes hows ANSI joins can be used across entities. Since 5.5 version, Couchbase server provides support for ANSI joins for joining documents using fields. Previous versions allowed index & lookup joins, which were supported in SDC only by querying directly through the SDK.

Relationships between entities across repositories can be one to one or one to many. By defining such relationships, a synchronized view of associated entities can be fetched.

10.1. Configuration

Associated entities can be fetched by annotating the entity’s property reference with @N1qlJoin. The prefix lks refers to left-hand side key space (current entity) and rks refers to the right-hand side key space (associated entity). The required element for @N1qlJoin annotation is the on clause, a boolean expression representing the join condition between the left-hand side (lks) and the right-hand side (rks), which can be fields, constant expressions or any complex N1QL expression. There could also be an optional where clause specified on the annotation for the join, similarly using lks to refer the current entity and rks to refer the associated entity.

Example 48. Annotation for ANSI Join
@Document
public class Author {
      @Id
      String id;

      String name;

      @N1qlJoin(on = "lks.name=rks.authorName")
      List<Book> books;

      @N1qlJoin(on = "lks.name=rks.name")
      Address address;
     ...
}

10.2. Lazy fetching

Associated entities can be lazily fetched upon the first access of the property, this could save on fetching more data than required when loading the entity. To load the associated entities lazily, @N1qlJoin annotation’s element fetchType has to be set to FetchType.LAZY. The default is FetchType.IMMEDIATE.

Example 49. Configuration for lazy fetch
@N1qlJoin(on = "lks.name=rks.authorName", fetchType = FetchType.LAZY)
List<Book> books;

10.3. ANSI Join Hints

10.3.1. Use Index Hint

index element on the @N1qlJoin can be used to provided the hint for the lks (current entity) index and rightIndex element can be used to provided the rks (associated entity) index.

10.3.2. Hash Join Hint

If the join type is going to be hash join, the hash side can be specified for the rks (associated entity). If the associated entity is on the build side, it can be specified as HashSide.BUILD else HashSide.PROBE.

10.3.3. Use Keys Hint

keys element on the @N1qlJoin annotation can be used to specify unique document keys to restrict the join key space.

11. Caching

This chapter describes additional support for caching and @Cacheable.

11.1. Configuration & Usage

Technically, caching is not part of spring-data, but is implemented directly in the spring core. Most database implementations in the spring-data package can’t support @Cacheable, because it is not possible to store arbitrary data.

Couchbase supports both binary and JSON data, so you can get both out of the same database.

To make it work, you need to add the @EnableCaching annotation and configure the cacheManager bean:

Example 50. AbstractCouchbaseConfiguration for Caching
@Configuration
@EnableCaching
public class Config extends AbstractCouchbaseConfiguration {
    // general methods

  @Bean
  public CouchbaseCacheManager cacheManager(CouchbaseTemplate couchbaseTemplate) throws Exception {
  CouchbaseCacheManager.CouchbaseCacheManagerBuilder builder = CouchbaseCacheManager.CouchbaseCacheManagerBuilder
      .fromConnectionFactory(couchbaseTemplate.getCouchbaseClientFactory());
    builder.withCacheConfiguration("mySpringCache", CouchbaseCacheConfiguration.defaultCacheConfig());
    return builder.build();
  }

The persistent identifier can then be used on the @Cacheable annotation to identify the cache manager to use (you can have more than one configured).

Once it is set up, you can annotate every method with the @Cacheable annotation to transparently cache it in your couchbase bucket. You can also customize how the key is generated.

Example 51. Caching example
@Cacheable(value="persistent", key="'longrunsim-'+#time")
public String simulateLongRun(long time) {
    try {
        Thread.sleep(time);
    } catch(Exception ex) {
        System.out.println("This shouldnt happen...");
    }
    return "I've slept " + time + " miliseconds.;
}

If you run the method multiple times, you’ll see a set operation happening first, followed by multiple get operations and no sleep time (which fakes the expensive execution). You can store whatever you want, if it is JSON of course you can access it through views and look at it in the Web UI.

Note that to use cache.clear() or catch.invalidate(), the bucket must have a primary key. :leveloffset: -1

12. Appendix

Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/repository-namespace-reference.adoc[] Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/repository-populator-namespace-reference.adoc[] Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/repository-query-keywords-reference.adoc[] Unresolved directive in index.adoc - include::../../../../spring-data-commons/src/main/asciidoc/repository-query-return-types-reference.adoc[] :leveloffset: -1