Multitenancy with spring-data-couchbase

java
connections

#1

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


#2

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.