Multitenancy with spring-data-couchbase

Background

I’m building a spring application that has multiple couchbase buckets. The master bucket contains data about other tenants configuration, while rest of the buckets are specific to tenant/customer and shares same DAOs, Services.

Problem

I want to save/retrieve documents to/from tenant specific buckets, but Spring data couchbase is static in nature and does not allow to dynamically bind buckets to repositories or to switch between buckets on runtime. Is there an way to map/bind the bucket to repository dynamically in spring data couchbase?

Configuration

I am initializing 3 tenants templates, setting master as default and want to switch between other 2 based on tenants on runtime.

@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.user"})
@EnableCouchbaseAuditing
public class CouchbaseMultiBucketConfig extends AbstractCouchbaseConfiguration {

private static final String ACCESS_FAILED = "Failed to access bucket template: ";

@Value("${bucket.master.name}")
private String masterBucketName;
@Value("${bucket.master.password}")
private String masterBucketPassword;
@Value("${bucket.master.host:#{null}}")
private String masterBucketHost;

@Value("${bucket.tenant1.name}")
private String tenant1BucketName;
@Value("${bucket.tenant1.password}")
private String tenant1BucketPassword;
@Value("${bucket.tenant1.host:#{null}}")
private String tenant1BucketHost;

@Value("${bucket.tenant2.name}")
private String tenant2BucketName;
@Value("${bucket.tenant2.password}")
private String tenant2ucketPassword;
@Value("${bucket.tenant2.host:#{null}}")
private String tenant2ucketHost;

@Value("${cb.hosts}")
private String hosts;

@Bean
@Qualifier("tenant1")
public Bucket tenant1Bucket() {
    return openBucket(tenant1BucketName, tenant1BucketPassword, tenant1BucketHost);
}

@Bean
@Qualifier("tenant2")
public Bucket tenant1Bucket() {
    return openBucket(tenant2BucketName, tenant2BucketPassword, tenant2BucketHost);
}

@Bean
@Qualifier("master")
public Bucket masterBucket() {
    return openBucket(masterBucketName, masterBucketPassword, masterBucketHost);
}

@Bean
@Qualifier("masterTemplate")
public CouchbaseTemplate masterTemplate() {
    try {
        final CouchbaseTemplate template = new CouchbaseTemplate(
                couchbaseClusterInfo(), //reuse the default bean
                masterBucket(), //the bucket is non-default
                mappingCouchbaseConverter(), translationService() //default beans here as well
        );
        template.setDefaultConsistency(getDefaultConsistency());
        return template;
    } catch (Exception ex) {
        throw new IllegalStateException(ACCESS_FAILED, ex);
    }
}

@Bean
@Qualifier("tenant1Template")
public CouchbaseTemplate tenant1Template() {
    try {
        final CouchbaseTemplate template = new CouchbaseTemplate(
                couchbaseClusterInfo(), //reuse the default bean
                tenant1Bucket(), //the bucket is non-default
                mappingCouchbaseConverter(), translationService() //default beans here as well
        );
        template.setDefaultConsistency(getDefaultConsistency());
        return template;
    } catch (Exception ex) {
        throw new IllegalStateException(ACCESS_FAILED, ex);
    }
}

@Bean
@Qualifier("tenant2Template")
public CouchbaseTemplate tenant2Template() {
    try {
        final CouchbaseTemplate template = new CouchbaseTemplate(
                couchbaseClusterInfo(), //reuse the default bean
                tenant2Bucket(), //the bucket is non-default
                mappingCouchbaseConverter(), translationService() //default beans here as well
        );
        template.setDefaultConsistency(getDefaultConsistency());
        return template;
    } catch (Exception ex) {
        throw new IllegalStateException(ACCESS_FAILED, ex);
    }
}

private Bucket openBucket(final String bucketName, final String bucketPassword, final String alternativeHost)
        throws IllegalStateException {
    try {
        return couchbaseCluster(alternativeHost).openBucket(bucketName, bucketPassword);
    } catch (Exception ex) {
        throw new IllegalStateException("Failed to open bucket " + bucketName, ex);
    }
}


@Override
public void configureRepositoryOperationsMapping(final RepositoryOperationsMapping baseMapping) {
    baseMapping.setDefault(masterTemplate());
}

@Override
protected List<String> getBootstrapHosts() {
    return parse(hosts);
}

@Override
protected String getBucketName() {
    return tenant1BucketName;
}

@Override
protected String getBucketPassword() {
    return tenant1BucketPassword;
}

private List<String> parse(final String hosts) {
    return Arrays.asList(hosts.split(","))
            .stream().map(in -> in.trim()).collect(Collectors.toList());
}

}

Repository

@Repository
public interface UserRepository extends CouchbaseRepository<User, String> {

User getByDocKey(final String docKey);

@Query("#{#n1ql.selectEntity} WHERE tenantId= $1")
List<User> findByTenantId(String tenantId);

}

Thanks

It is not possible to change the underlying bucket mapping for the repository in the runtime after the configuration is done, afaik it can be changed only in the java config for the repository operation mapping.

Hello @subhashni ,
I have a requirement to reload cluster,Bucket and CouchbaseTemplate with new username,passsword .My couchbase username password changes every 25 days so i want to make new instance of Cluster,Bucket and create CouchbaseTemplate from them.Then replace it in the applicationContext.Is there a possibility to acheive this without affecting couchbase Transactions?
I am trying to generate
CouchbaseTemplate couchbaseTemplateNew =CouchbaseTemplate(com.couchbase.client.java.cluster.ClusterInfo clusterInfo, com.couchbase.client.java.Bucket client, CouchbaseConverter converter,TranslationService translationService)
replace existing applicationContext.registerSingletiopn("couchbaseTemplate",couchbaseTemplateNew ).Will this work out without impacting live transactions?

Hi @shah.hiren.t,

Yes, it may be possible to use beanfactory’s destory and create singleton methods, but the bean lifecycle management is handled by the core Spring framework, it would be best to contact the spring community directly.
The operations would be affected if the repository is holding on to the old bean instance.