© 2014-2019 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
:
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.
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>4.1.1</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 and milestone releases from the milestone repository. Here is an example on how to use the current SNAPSHOT dependency:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<version>4.2.0-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:
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.
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:
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?
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.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.bucket} 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 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();
7. Transaction Support
Couchbase supports Distributed Transactions. This section documents on how to use it with Spring Data Couchbase.
7.1. Requirements
-
Couchbase Server 6.5 or above.
-
Couchbase Java client 3.0.0 or above. It is recommended to follow the transitive dependency for the transactions library from maven.
-
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.
7.2. Getting Started & Configuration
The couchbase-transactions
artifact needs to be included into your pom.xml
if maven is being used (or equivalent).
-
Group:
com.couchbase.client
-
Artifact:
couchbase-transactions
-
Version: latest one, i.e.
1.0.0
Once it is included in your project, you need to create a single Transactions
object. Conveniently, it can be part of
your spring data couchbase AbstractCouchbaseConfiguration
implementation:
@Configuration
static class Config extends AbstractCouchbaseConfiguration {
// Usual Setup
@Override public String getConnectionString() { /* ... */ }
@Override public String getUserName() { /* ... */ }
@Override public String getPassword() { /* ... */ }
@Override public String getBucketName() { /* ... */ }
@Bean
public Transactions transactions(final Cluster couchbaseCluster) {
return Transactions.create(couchbaseCluster, TransactionConfigBuilder.create()
// The configuration can be altered here, but in most cases the defaults are fine.
.build());
}
}
Once the @Bean
is configured, you can autowire it from your service (or any other class) to make use of it. Please
see the Reference Documentation
on how to use the Transactions
class. Since you need access to the current Collection
as well, we recommend you to also
autowire the CouchbaseClientFactory
and access it from there:
@Autowired
Transactions transactions;
@Autowired
CouchbaseClientFactory couchbaseClientFactory;
public void doSomething() {
transactions.run(ctx -> {
ctx.insert(couchbaseClientFactory.getDefaultCollection(), "id", "content");
ctx.commit();
});
}
7.3. Object Conversions
Since the transactions library itself has no knowledge of your spring data entity types, you need to convert it back and
forth when reading/writing to interact properly. Fortunately, all you need to do is autowire the MappingCouchbaseConverter
and
utilize it:
@Autowired
MappingCouchbaseConverter mappingCouchbaseConverter;
public void doSomething() {
transactions.run(ctx -> {
Airline airline = new Airline("demo-airline", "at");
CouchbaseDocument target = new CouchbaseDocument();
mappingCouchbaseConverter.write(airline, target);
ctx.insert(couchbaseClientFactory.getDefaultCollection(), target.getId(), target.getContent());
ctx.commit();
});
}
The same approach can be used on read:
TransactionGetResult getResult = ctx.get(couchbaseClientFactory.getDefaultCollection(), "doc-id");
CouchbaseDocument source = new CouchbaseDocument(getResult.id());
source.setContent(getResult.contentAsObject());
Airline read = mappingCouchbaseConverter.read(Airline.class, source);
We are also looking into tighter integration of the transaction library into the spring data library ecosystem. :leveloffset: -1
8. 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