I've done a project that I can create multiple dataSources with your specific changeSets, so if you need to add another dataSource, it would just change your application.yml, no longer needing to change the code.
But if not use, just remove the liquibase that works too!
On each hit for your controller, you need to get an X-TenantId header, which will change your ThreadLocal, which in turn changes the datasource according with tenant
Code complete: https://github.com/dijalmasilva/spring-boot-multitenancy-datasource-liquibase
application.yml
spring:
dataSources:
- tenantId: db1
url: jdbc:postgresql://localhost:5432/db1
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
liquibase:
enabled: true
default-schema: public
change-log: classpath:db/master/changelog/db.changelog-master.yaml
- tenantId: db2
url: jdbc:postgresql://localhost:5432/db2
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
- tenantId: db3
url: jdbc:postgresql://localhost:5432/db3
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
TenantContext
public class TenantContext {
private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
static String getCurrentTenant() {
return currentTenant.get();
}
static void setCurrentTenant(String tenant) {
currentTenant.set(tenant);
}
static void clear() {
currentTenant.remove();
}
}
Filter to Controllers
public class TenantFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final String X_TENANT_ID = "X-TenantID";
final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
final String tenantId = httpServletRequest.getHeader(X_TENANT_ID);
if (tenantId == null) {
final HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"error\": \"No tenant header supplied\"}");
response.getWriter().flush();
TenantContext.clear();
return;
}
TenantContext.setCurrentTenant(tenantId);
filterChain.doFilter(servletRequest, servletResponse);
}
}
Configuration class if use liquibase
@Configuration
@ConditionalOnProperty(prefix = "spring.liquibase", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(LiquibaseProperties.class)
@AllArgsConstructor
public class LiquibaseConfiguration {
private LiquibaseProperties properties;
private DataSourceProperties dataSourceProperties;
@Bean
@DependsOn("tenantRoutingDataSource")
public MultiTenantDataSourceSpringLiquibase liquibaseMultiTenancy(Map<Object, Object> dataSources,
@Qualifier("taskExecutor") TaskExecutor taskExecutor) {
// to run changeSets of the liquibase asynchronous
MultiTenantDataSourceSpringLiquibase liquibase = new MultiTenantDataSourceSpringLiquibase(taskExecutor);
dataSources.forEach((tenant, dataSource) -> liquibase.addDataSource((String) tenant, (DataSource) dataSource));
dataSourceProperties.getDataSources().forEach(dbProperty -> {
if (dbProperty.getLiquibase() != null) {
liquibase.addLiquibaseProperties(dbProperty.getTenantId(), dbProperty.getLiquibase());
}
});
liquibase.setContexts(properties.getContexts());
liquibase.setChangeLog(properties.getChangeLog());
liquibase.setDefaultSchema(properties.getDefaultSchema());
liquibase.setDropFirst(properties.isDropFirst());
liquibase.setShouldRun(properties.isEnabled());
return liquibase;
}
}
Code complete: https://github.com/dijalmasilva/spring-boot-multitenancy-datasource-liquibase