18
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.
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.
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
Aplicação completa no github https://github.com/fabriciolfj/multitenancy