Spring Data Couchbase 4 Multibuckets in Spring Boot 2

By default, Spring Data Couchbase implements single-bucket configuration. In this default implementation, all POJO (Plain Old Java Object) models/documents will be stored in a single bucket. This is possible, because Couchbase is a NoSQL database which can store schemaless data. To differentiate between type of documents, Spring Data adds _class field that contains the fully-qualified class name of the POJO.

{
  "name": "Jane Doe",
  "createdAt": 1624102211338,
  "_class": "id.gultom.model.Customer",
  "updatedAt": 1624102211338
}

Spring Data Couchbase has an extensive documentation available here, but unfortunately the section about multibucket(s) implementation is missing. There are also some tutorials like this one, but most of them are only for single-bucket implementation. There are some references regarding multi-buckets implementation (like this Github Ticket) scattered in the internet, but I couldn’t find a single comprehensive example. So I decided to post the result of my research here.

⚠️ Please be aware that this implementation is for the Spring Data Couchbase 4.2.1. The older or newer version may require different approach — although I believe this is compatible for the same major versions (4.x). Also, in this example I only use the non-reactive repository. Implementing reactive repository may require some adjustments as well.

In order to implement multi-buckets in Spring Boot application, we need to setup a configuration class with custom Couchbase template beans for each buckets. Only model/document that is mapped to custom Couchbase template that will have separate bucket. Otherwise they will be stored in the default bucket. In this example, we define supplierTemplate and customerTemplate for Supplier and Customer models respectively.

package id.gultom.config;

import id.gultom.model.Customer;
import id.gultom.model.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping;

@Slf4j
@Configuration
@EnableCouchbaseRepositories(basePackages = "id.gultom.repository.couchbase")
public class CouchbaseConfig extends AbstractCouchbaseConfiguration {
    @Value("${spring.couchbase.connection-string}")
    private String connectionString;

    @Value("${spring.couchbase.username}")
    private String username;

    @Value("${spring.couchbase.password}")
    private String password;

    // this bucket will be used by models/repositories
    // that is not mapped to custom couchbase templates
    @Value("${spring.data.couchbase.bucket-name:Default}")
    private String defaultBucketName;

    @Autowired
    private MappingCouchbaseConverter mappingCouchbaseConverter;

    @Override
    public String getConnectionString() {
        return this.connectionString;
    }

    @Override
    public String getUserName() {
        return this.username;
    }

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

    @Override
    public String getBucketName() {
        return this.defaultBucketName;
    }

    @Override
    protected void configureRepositoryOperationsMapping(RepositoryOperationsMapping mapping) {
        // only model that is mapped to custom template will be mapped to non-default bucket
        mapping.mapEntity(Supplier.class, supplierTemplate());
        mapping.mapEntity(Customer.class, customerTemplate());
    }

    @Bean
    public CouchbaseTemplate supplierTemplate() {
        // define the custom template bucket name
        return customCouchbaseTemplate(customCouchbaseClientFactory("Supplier"));
    }

    @Bean
    public CouchbaseTemplate customerTemplate() {
        // define the custom template bucket name
        return customCouchbaseTemplate(customCouchbaseClientFactory("Customer"));
    }

    // do not use couchbaseTemplate for the name of this method, otherwise the value of that been
    // will be used instead of the result from this call (the client factory arg is different)
    public CouchbaseTemplate customCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory) {
        return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter);
    }

    // do not use couchbaseClientFactory for the name of this method, otherwise the value of that bean will
    // will be used instead of this call being made ( bucketname is an arg here, instead of using bucketName() )
    public CouchbaseClientFactory customCouchbaseClientFactory(String bucketName) {
        return new SimpleCouchbaseClientFactory(getConnectionString(), authenticator(), bucketName);
    }
}

That’s all! ?

The remaining POJO and repository classes can be defined just like the default/single-bucket implementation. Because as we can see in the configuration class above, we already explicitly map the POJOs with the custom Couchbase template and the repositories should be mapped to the POJOs like example below.

package id.gultom.repository.couchbase;

import id.gultom.model.Supplier;
import org.springframework.data.couchbase.repository.CouchbaseRepository;

public interface SupplierRepository extends CouchbaseRepository<Supplier, String> {

}

Besides that, we just need to make sure the buckets and indexes are created properly in the Couchbase database server our application is connected to. Because Spring Data Couchbase doesn’t automatically create them for us (like Hibernate).

As a reference, this is the relevant part of the pom.xml for this project:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>id.gultom</groupId>
	<artifactId>rest-api</artifactId>
        ...
	<dependencies>
                ...
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-couchbase</artifactId>
		</dependency>
                ...
	</dependencies>
        ...
</project>

Obviously, my configuration is not the only way to implement multi-buckets. But this is the configuration that I’ve personally tested and apparently working well. I’d appreciate if you can give a comment if you thing any improvement can be made.

The full code (including minimal Couchbase setup) can be found in this Github repository https://github.com/yohanesgultom/spring-boot-microservice.

Enjoy! ?

5 3 votes
Article Rating
Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments