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
Top comments (0)