DEV Community

fabriciolfj
fabriciolfj

Posted on

3 1

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)
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {

    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}

Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay