Multitenancy com spring boot e flyway

Houve uma situação em que minha aplicação, necessitava gravar informações em duas base de dados. Adotei uma abordagem menos efetiva, onde fiz uso do datasource configurado no yml e criei outro datasource direto na app (um novo bean). Código ficou verboso e suscetível a erros.
Existe uma abordagem mais elegante, para sanar a situação relatada acima, que é conhecida como multitenancy ou multi-inquilino. Aplicativo que permite diferentes inquilinos trabalharem com o mesmo, sem ver os dados uns dos outros.
Para atingir esse propósito, o datasource de cada inquilino é configurado de forma dinâmica, como veremos abaixo.

Vamos simular 2 inquilinos, desta forma temos o seguinte application.yml:

tenants:
  datasources:
    financeiro-01:
      jdbcUrl: jdbc:h2:mem:financeiro
      driverClassName: org.h2.Driver
      username: sa
      password: password
    estoque-01:
      jdbcUrl: jdbc:h2:mem:estoque
      driverClassName: org.h2.Driver
      username: sa
      password: password

spring:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
  flyway:
    enabled: false #para gerar o schema quando solicitado, pois inicialmente não teremos ninguem registrado (nenhum inquilino)

Uma forma de isolar cada inquilino, fiz o uso da ThreadLocal, conforme exemplo abaixo:

public class ThreadTenantStorage {

    private static ThreadLocal<String> currentTenant = new ThreadLocal<>();

    public static void setTenantId(final String tenantId) {
        currentTenant.set(tenantId);
    }

    public static String getTenantId() {
        return currentTenant.get();
    }

    public static void clear() {
        currentTenant.remove();
    }
}

Fiz uso dessa abordagem em um aplicação rest, necessitando criar o interceptor abaixo, cuja função é que pegar o valor da chave x-tenant (que conterá o nome do inquilino) informado no header da requisição, e colocá-lo no store de threads:

@Component
public class ExampleTenantInterceptor implements WebRequestInterceptor {

    public static final String TENANT_HEADER = "X-tenant";

    @Override
    public void preHandle(WebRequest webRequest) throws Exception {
        ThreadTenantStorage.setTenantId(webRequest.getHeader(TENANT_HEADER));
    }

    @Override
    public void postHandle(WebRequest webRequest, ModelMap modelMap) throws Exception {

    }

    @Override
    public void afterCompletion(WebRequest webRequest, Exception e) throws Exception {

    }
}

Por fim, registrando o interceptor no contexto do spring:

@Configuration
@RequiredArgsConstructor
public class WebConfiguration implements WebMvcConfigurer {

    private final ExampleTenantInterceptor exampleTenantInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addWebRequestInterceptor(exampleTenantInterceptor);
    }
}

Agora iniciamos a configuração dinâmica do datasource.
Fiz uso da classe AbstractRoutingDataSource, que permite selecionar qual conexão utilizar.

public class TenantRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return ThreadTenantStorage.getTenantId();
    }
}

Injetei as propriedades:

@Log4j2
@Component
@ConfigurationProperties(prefix = "tenants")
public class DataSourcesProperties {

    public Map<Object, Object> datasources = new LinkedHashMap<>();

    public Map<Object, Object> getDataSources() {
        return datasources;
    }

    public void setDatasources(Map<String, Map<String, String>> datasources) {
        log.info("map: {}", datasources);
        datasources
                .forEach((key, value) -> {
                    log.info("key: {}, value: {}", key, value);
                    this.datasources.put(key, convert(value));
                });
    }

    public DataSource convert(Map<String, String> source) {
        return DataSourceBuilder.create()
                .url(source.get("jdbcUrl"))
                .driverClassName(source.get("driverClassName"))
                .username(source.get("username"))
                .password(source.get("password"))
                .build();
    }
}

Por fim a configuração propriamente dita do datasource:

@Configuration
@RequiredArgsConstructor
public class DataSourceConfiguration {

    private final DataSourcesProperties dataSourcesProperties;

    @Bean
    public DataSource dataSource() {
        final var customDataSource = new TenantRoutingDataSource();
        customDataSource.setTargetDataSources(dataSourcesProperties.getDataSources());
        return customDataSource;
    }

    @PostConstruct
    public void migrate() {
        for (Object dataSource : dataSourcesProperties
                .getDataSources()
                .values()) {
            DataSource source = (DataSource) dataSource;
            Flyway flyway = Flyway.configure().dataSource(source).load();
            flyway.migrate();
        }
    }
}

Ja tenho uma aplicação pronta para uso de 2 inquilinos.
Aplicação completa no github https://github.com/fabriciolfj/multitenancy

14