© 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
Project Information
-
Version control - https://github.com/spring-projects/spring-data-couchbase
-
Bugtracker - https://jira.springsource.org/browse/DATACOUCH
-
Release repository - https://repo.spring.io/libs-release
-
Milestone repository - https://repo.spring.io/libs-milestone
-
Snapshot repository - https://repo.spring.io/libs-snapshot
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
becameimport org.springframework.data.annotation.Id
-
com.couchbase.client.java.repository.annotation.Field
becameimport 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:
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:
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
becameorg.springframework.data.couchbase.repository.Query
-
org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository
has been removed. Consider extendingReactiveSortingRepository
orReactiveCouchbaseRepository
-
org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository
has been removed. Consider extendingPagingAndSortingRepository
orCouchbaseRepository
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
:
@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:
@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:
<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:
<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.
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:
@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:
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:
@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:
{
"_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
:
@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:
{
"_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:
@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:
{
"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
):
@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
andCouchbaseList
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 justLong
). 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:
@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:
<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:
@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.
@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:
@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
.
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:
@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 .
.
@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.
@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.
@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.
@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:
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?
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:
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 thedelete 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:
@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
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 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
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 |
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 oneCompositeQueryIndex
should be created, this annotation will take a list of them.
For example, this is how you define a composite index on an entity:
@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:
@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:
@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
andlastName
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.
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:
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:
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.
@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.
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:
// 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:
User modified = couchbaseTemplate
.upsertById(User.class)
.withDurability(DurabilityLevel.MAJORITY)
.one(user);
In a similar fashion, you can perform a N1QL operation:
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.
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.
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:
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;
@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)));
}
}
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.
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)));
}
}
@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
-
the configuration class to be annotated with @EnableTransactionManagement;
-
the service object with the annotated methods must be annotated with @Service;
-
the body of the method is executed in a transaction.
-
the service object with the annotated methods must be obtained via @Autowired.
-
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)).
@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
@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"));
});
@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);
-
inScope()/inCollection() of the template fluent api
-
withScope()/withCollection() of the template/repository object
-
annotation of the repository method
-
annotation of the repository interface
-
annotation of the entity object
-
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.
-
@Encrypted defines a field as encrypted.
-
@Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED) defines a field that may or may not be encrypted when read. It will be encrypted when written.
-
@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
@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();
}
}
@Document
public class AddressWithEncStreet extends Address {
private @Encrypted String encStreet;
.
.
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setCity("Santa Clara");
address.setEncStreet("Olcott Street");
addressEncryptedRepository.save(address);
{
"_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.
@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
.
@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:
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.
@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