Search code examples
springspring-bootspring-mvcspring-data-jpamulti-tenant

Spring Boot Multitenancy - Hibernate - Use ddl-auto to update all schemas when entity structure changes


I am new to Spring Boot and trying to implement multi-tenancy architecture using Spring boot 2, hibernate and flyway. I was referring to tutorial https://reflectoring.io/flyway-spring-boot-multitenancy/ to understand the concepts and was able to implement the architecture as mentioned.

However, if I add a new field entity classes, everything breaks because hibernate does not create new fields in tenant databases. From reading theory and stackoverflow questions, I understand that flyway is to solve this problem. However, i am not able to make it work.

Can somebody tell me where I am going wrong. My requirement is - When I add a new field to entity class, all tables in all tenant databases should get updated with that field. Below is the code

Application Properties

spring:
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
  flyway:
    enabled: false
tenants:
  datasources:
    vw:
      jdbcUrl: jdbc:mysql://localhost:3306/vw
      driverClassName: com.mysql.jdbc.Driver
      username: vikky
      password: Test@123
    bmw:
      jdbcUrl: jdbc:mysql://localhost:3306/bmw
      driverClassName: com.mysql.jdbc.Driver
      username: vikky
      password: Test@123

Datasource Configuration

@Configuration
public class DataSourceConfiguration {

    private final DataSourceProperties dataSourceProperties;

    public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    @Bean
    public DataSource dataSource() {
        TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
        customDataSource.setTargetDataSources(dataSourceProperties.getDatasources());
        return customDataSource;
    }

    @PostConstruct
    public void migrate() {
        dataSourceProperties
                .getDatasources()
                .values()
                .stream()
                .map(dataSource -> (DataSource) dataSource)
                .forEach(this::migrate);
    }

    private void migrate(DataSource dataSource) {
        Flyway flyway = Flyway.configure().dataSource(dataSource).load();
        flyway.migrate();
    }
}

DataSource properties

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

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

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

    public void setDatasources(Map<String, Map<String, String>> datasources) {
        datasources
                .forEach((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();
    }
}

Tenant Routing Data Source

public class TenantRoutingDataSource extends AbstractRoutingDataSource {

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

Header Interceptor

@Component
public class HeaderTenantInterceptor implements WebRequestInterceptor {

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

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

    @Override
    public void postHandle(WebRequest request, ModelMap model) throws Exception {
        ThreadTenantStorage.clear();
    }

    @Override
    public void afterCompletion(WebRequest request, Exception ex) throws Exception {

    }
}

There are other classes as well like Web Configuration, controllers etc. but I don't they are required to be posted here.


Solution

  • After lot of research, I understood that flyway is required only in case of production, where we do not want to update table definition using ddl-auto=true. Since that was not the case with me, I added below configuration to update all schemas according to entity structure

    @Configuration
    public class AutoDDLConfig
    {
    
        @Value("${spring.datasource.username}")
        private String username;
    
        @Value("${spring.datasource.password}")
        private String password;
    
        @Value("${schemas.list}")
        private String schemasList;
    
        @Bean
        public void bb()
        {
    
            if (StringUtils.isBlank(schemasList))
            {
                return;
            }
    
            String[] tenants = schemasList.split(",");
    
            for (String tenant : tenants)
            {
                tenant = tenant.trim();
                DriverManagerDataSource dataSource = new DriverManagerDataSource();
                dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // Change here to MySql Driver
                dataSource.setSchema(tenant);
                dataSource.setUrl("jdbc:mysql://localhost/" + tenant
                        + "?autoReconnect=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=Asia/Kolkata&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true");
                dataSource.setUsername(username);
                dataSource.setPassword(password);
    
                LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
                emfBean.setDataSource(dataSource);
                emfBean.setPackagesToScan("com"); // Here mention JPA entity path / u can leave it scans all packages
                emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
                emfBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
                Map<String, Object> properties = new HashMap<>();
    
                properties.put("hibernate.hbm2ddl.auto", "update");
                properties.put("hibernate.default_schema", tenant);
    
                emfBean.setJpaPropertyMap(properties);
                emfBean.setPersistenceUnitName(dataSource.toString());
                emfBean.afterPropertiesSet();
            }
    
        }
    
    }